3

I'm coming from vim, which has a gq operator that acts much like emacs' fill-paragraph function (M-q). There's a 'formatprg' setting that allows you to point gq at an external program for its formatting decisions, and I've found that using par yields much nicer results than vim's default formatting logic.

I'd like to do something equivalent in emacs (get it to use par). I see there's a fill-paragraph-function variable, and I know that basically anything is possible in emacs, but I'm not sure how to go about wiring it in. How could I do so?

  • I've never used it, but the basic steps would be to write a wrapper function that a) finds the bounds of the paragraph, b) sends the characters within those bounds to par for processing, c) takes the formatted text returned by par, and d) replaces the original text with the new text. – Dan Aug 18 '16 at 13:00
  • Paragraph filling in emacs is handled in major modes. It's possible, and relatively straight forward, to mark a paragraph and run par on it, and have the return value replace the old paragraph. However, this approach is at odds with normal practice, and you might end up with a brittle amalgam of custom code that needs to be updated for each mode you use. Before going down that route, perhaps you can see if the built-in paragraph filling features can be tweaked to do what you want. – Tyler Aug 18 '16 at 15:04
  • TIL about the par utility; it's man page is beautifully written. Thanks! – Kaushal Modi Aug 19 '16 at 4:37
3

Essentially you want your function to do the equivalent of marking the paragraph and typing C-uM-| par RET

Something like:

(defun my-fill-paragraph-using-par (&optional _justify)
  "Invoke shell command `par' on the current paragraph."
  (interactive)
  (save-excursion
    (mark-paragraph)
    (forward-whitespace 1)
    (shell-command-on-region (point) (mark) "par" nil :replace))
  t) ;; Don't return nil. See variable `fill-paragraph-function'.
3

Here I have taken what @phils had posted and have modified it to:

  • Take more par options
  • Apply par formatting only on the region, without limiting to paragraph marking because mark-paragraph relies on the major mode to work correctly.
  • Attempt to make this work on code comments (by marking comment-start characters as Quote characters for par).
  • Attempt to handle par errors more gracefully and try not to mess up the user's original buffer in the event of error

How to use the below code

  1. Evaluate the below code
  2. M-h M-H if you want to fill the region in a paragraph

Code

;; par - http://www.nicemice.net/par/
(when (executable-find "par")
  (defun modi/par-fill-region (begin end par-option)
    "Use `par' executable to fill region between BEGIN and END.

If PAR-OPTION is \\='(4) `\\[universal-argument]', also justify the text.
If PAR-OPTION is \\='(16) `\\[universal-argument] \\[universal-argument]', try to
make all lines of almost equal lengths instead of justifying.

See `man par' for more information."
    (interactive "r\nP")
    (let* ((width-str (number-to-string fill-column))
           (err-buf "*par Error*")
           (par-cmd
            (concat "par "
                    "w" width-str " " ; set fill width
                    ;; Body characters: . , ? upper-case lower-case
                    (shell-quote-argument "B=.,?_A_a") " "
                    ;; Quote characters: space > | <comment-start char>
                    (shell-quote-argument
                     (concat "Q=_s>|" (when comment-start comment-start))) " "
                     ;; Allow bodiless characters like *, space, .. to inc/dec by
                     ;; up to 10 characters to adjust line width.
                     "r10"
                     "T4" ; Expand tab chars to 4 spaces
                     ;; Prefixes may not contain any trailing body characters, and
                     ;; suffixes may not contain any leading body characters
                     "b"
                     "e" ; Expel/remove superfluous lines
                     (when (equal '(16) par-option)
                       "f") ; Try to make all lines of nearly the same length
                     "g" ; Make a better guess at inserting line breaks
                     (when (equal '(4) par-option)
                       "j") ; Justify the lines by inserting spaces between words
                     "q" ; Insert blank lines before/after quoted text
                     "R" ; Throw an error if a word length exceeds the fill width
                     "E" )) ; Send error to stderr instead of stdout
           (before-text (buffer-substring-no-properties begin end))
           par-ret
           ;; Do the formatting in a temp buffer so that the text in the original
           ;; buffer doesn't get corrupted in case `par' fails due to some error.
           (after-text (with-temp-buffer
                         (insert before-text)
                         (setq par-ret (shell-command-on-region
                                        (point-min) (point-max)
                                        par-cmd nil :replace
                                        err-buf :display-error-buffer))
                         (buffer-substring-no-properties (point-min) (point-max)))))
      ;; If 1 is returned, error occurred in the cmd execution; 0 - no error
      (if (= 1 par-ret)
          (progn
            ;; Switch to the error buffer
            (switch-to-buffer-other-window err-buf)
            (special-mode)) ; Set this mode so that you can quit it quickly using C-u q
        ;; If no error occurred, do below in the original buffer 
        (delete-region begin end)
        (insert after-text))
      (message "Executed `%s' on the region" par-cmd)))
  ;; Rationale for the below binding is that it flows very well when selecting a
  ;; paragraph and then filling that region: "M-h M-H"
  (global-set-key (kbd "M-H") #'modi/par-fill-region))
  • Very cool! I accepted @phils answer since it was the original, but thanks for contributing. There's a lot I can learn from here. – ivan Aug 20 '16 at 23:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.