How do I get Emacs to fill sentences, but not paragraphs?

I've seen at least two recommendations on StackOverflow to insert newlines between sentences when editing LaTeX documents. The reason being that the practice facilitates source control, diffing, and collaborative editing.

I'm basically convinced, but I'm lazy, and I don't want to have to think about it.

So I'm searching for some emacs incantation to handle it for me. Could be a minor mode, could be a set of variables that need to be set.

I think what I don't want is

I think what I do want is

That is:

chat chat chat.
A new sentence
with goofed up wrapping that needs to be fixed.
Mumble mumble

Transformed to:

chat chat chat.
A new sentence with goofed up wrapping that needs to be fixed.
Mumble mumble

Your comments and suggestions are appreciated.


Edit: The suggestion by Jouni K. Seppänen pointed me at LaTeX-fill-break-at-separators, which suggests that emacs almost knows how to do this already. Anyway, I'm off to read some code, and will report back. Thanks again.


More general version of the same question: Editor showdown: Maintain newlines at the ends of sentences. Thanks, dreeves.

Upvotes: 31

Views: 5473

Answers (10)

agarttha
agarttha

Reputation: 11

I wrote the following which loops over a region and inserts newlines. Instead of using forward-sentence which didn't work for me, I use re-search-forward "[.?!][]\"')}]*\\( \\)", which finds all sentences followed only by two spaces (the regexp is a modified sentence-end). The newline is made using newline-and-indent.

(defun fill-sentences-in-paragraph ()
  "Put a newline at the end of each sentence in paragraph."
  (interactive)
  (save-excursion
    (mark-paragraph)
    (call-interactively 'fill-sentences-in-region)))

(defun fill-sentences-in-region (start end)
  "Put a newline at the end of each sentence in region."
  (interactive "*r")
  (call-interactively 'unfill-region)
  (save-excursion
    (goto-char start)
    (while (re-search-forward "[.?!][]\"')}]*\\(  \\)" end t)
      (newline-and-indent))))

To be able to fix improperly formatted text such as the example "chat chat chat...", fill-sentences-in-region first calls unfill-region which gets rid of sentence-breaking whitespace:

   (defun unfill-region (beg end)
      "Unfill the region, joining text paragraphs into a
       single logical line.  This is useful, e.g., for use
       with 'visual-line-mode'."
      (interactive "*r")
      (let ((fill-column (point-max)))
        (fill-region beg end)))

I use visual-line-mode and replace my default paragraph fill M-q to fill-sentences-in-paragraph with (global-set-key "\M-q" 'fill-sentences-in-paragraph).

Upvotes: 1

pnj
pnj

Reputation: 1447

An alternative approach would be to leave your .tex file as is, and use a tool like latexdiff (described in this StackExchange post) instead of Unix diff. This produces a .tex file with Word-style track changes marks, and handles whitespace correctly so you don't have to worry about where your sentences end.

Upvotes: 1

Ivan Andrus
Ivan Andrus

Reputation: 5301

I have been meaning to do this forever and I recently found this blog post which worked fairly well for me. So here is (a slightly modified version of) what I have been using for a few days.

(defun auto-fill-by-sentences ()
  (if (looking-back (sentence-end))
      ;; Break at a sentence
      (progn
        (LaTeX-newline)
        t)
    ;; Fall back to the default
    (do-auto-fill)))
(add-hook 'LaTeX-mode-hook (lambda () (setq auto-fill-function 'auto-fill-by-sentences)))

;; Modified from http://pleasefindattached.blogspot.com/2011/12/emacsauctex-sentence-fill-greatly.html
(defadvice LaTeX-fill-region-as-paragraph (around LaTeX-sentence-filling)
  "Start each sentence on a new line."
  (let ((from (ad-get-arg 0))
        (to-marker (set-marker (make-marker) (ad-get-arg 1)))
        tmp-end)
    (while (< from (marker-position to-marker))
      (forward-sentence)
      ;; might have gone beyond to-marker---use whichever is smaller:
      (ad-set-arg 1 (setq tmp-end (min (point) (marker-position to-marker))))
      ad-do-it
      (ad-set-arg 0 (setq from (point)))
      (unless (or (looking-back "^\\s *")
                  (looking-at "\\s *$"))
        (LaTeX-newline)))
    (set-marker to-marker nil)))
(ad-activate 'LaTeX-fill-region-as-paragraph)

Upvotes: 5

Rob
Rob

Reputation: 31

(defun wrap-at-sentences ()
  "Fills the current paragraph, but starts each sentence on a new line."
  (interactive)
  (save-excursion
    ;; Select the entire paragraph.
    (mark-paragraph)
    ;; Move to the start of the paragraph.
    (goto-char (region-beginning))
    ;; Record the location of the end of the paragraph.
    (setq end-of-paragraph (region-end))
    ;; Wrap lines with 'hard' newlines (i.e., real line breaks).
    (let ((use-hard-newlines 't))
      ;; Loop over each sentence in the paragraph.
      (while (< (point) end-of-paragraph)
        ;; Determine the region spanned by the sentence.
        (setq start-of-sentence (point))
        (forward-sentence)
        ;; Wrap the sentence with hard newlines.
        (fill-region start-of-sentence (point))
        ;; Delete the whitespace following the period, if any.
        (while (char-equal (char-syntax (preceding-char)) ?\s)
          (delete-char -1))
        ;; Insert a newline before the next sentence.
        (insert "\n")))))

(global-set-key (kbd "M-q") 'wrap-at-sentences)

Upvotes: 3

C.K.
C.K.

Reputation: 66

I like Chris Conway's macro a lot but it only works after you manually line-break each sentence. I'm a lazy guy so I want emacs to do it for me. This morning I finally sat down and looked into the problem. The solution I have now is to hack the built-in macro fill-region-as-paragraph.

After applying the following hack, a new option newline-after-sentence will be set to true. The standard M-q (fill-paragraph) will automatically fill and create line-breaks between sentences. Note that tests are only done with GNU Emacs 23.3.1 — use it at your own risk.

The full macro is long so I won't post it here. The idea is to add the following loops in fill-region-as-paragraph

...

;; Insert a line break after each sentence
(while (< (point) to)
  (forward-sentence)
  (if (< (point) to) (fill-newline)))

;; This is the actual filling loop.
(goto-char from)
(let (sentbeg sentend)
  (while (< (point) to)
    (setq sentbeg (point))
    (end-of-line)
    (setq sentend (point))
    (fill-one-line sentbeg sentend justify) ;; original filling loop
    (forward-line)))))

...

You can find the full macro in my git repository. Some details are also written in my blog. In case you don't want to read my poor English, you can simply use

$ curl http://fermi.mycloudnas.com/cgit.cgi/fill/plain/hack.el >> ~/.emacs

to append the hack to your ~/.emacs and give it a try. Comments and bug reports are all welcome.

Upvotes: 1

Chris Conway
Chris Conway

Reputation: 55989

Here's what I use, which was mostly cribbed from Luca de Alfaro:

(defun fill-sentence ()
  (interactive)
  (save-excursion
    (or (eq (point) (point-max)) (forward-char))
    (forward-sentence -1)
    (indent-relative t)
    (let ((beg (point))
          (ix (string-match "LaTeX" mode-name)))
      (forward-sentence)
      (if (and ix (equal "LaTeX" (substring mode-name ix)))
          (LaTeX-fill-region-as-paragraph beg (point))
        (fill-region-as-paragraph beg (point))))))

I bind this to M-j with

(global-set-key (kbd "M-j") 'fill-sentence)

The references to "LaTeX" are for AUCTeX support. If you don't use AUCTeX, the let can be simplified to

(let (beg (point))
  (forward-sentence)
  (fill-region-as-paragraph beg (point)))

Upvotes: 10

dreeves
dreeves

Reputation: 26932

If the other answers are too automatic, here's a semiautomatic approach. It's basically what you would do repeatedly if you were going to manually reformat, but condensed so you can hit a single key repeatedly instead.

;; - go to the end of the line,
;; - do ^d to suck the previous line onto this one, 
;; - make sure there's only one space between the now-concatenated
;;   lines, and then 
;; - jump to the end and hit space so that (with auto-fill-mode)
;;   the line nicely rewraps itself:
;;   (turn on auto-fill-mode with M-x auto-fill-mode)
(defalias 'fill-sentence
  (read-kbd-macro "C-e C-d SPC M-x just- one- space RET C-e SPC <backspace>"))

(define-key global-map [f4] 'fill-sentence)  ; or whatever key you like

Upvotes: 1

scottfrazer
scottfrazer

Reputation: 17327

May not work in all circumstances, but:

(defun my-fill-sentence ()
  "Fill sentence separated by punctuation or blank lines."
  (interactive)
  (let (start end)
    (save-excursion
      (re-search-backward "\\(^\\s-*$\\|[.?!]\\)" nil t)
      (skip-syntax-forward "^w")
      (setq start (point-at-bol)))
    (save-excursion
      (re-search-forward "\\(^\\s-*$\\|[.?!]\\)" nil t)
      (setq end (point-at-eol)))
    (save-restriction
      (narrow-to-region start end)
      (fill-paragraph nil))))

To make it work with auto-fill-mode, add (setq normal-auto-fill-function 'my-fill-sentence) to your LaTeX mode hook (I think).

Upvotes: 2

jrockway
jrockway

Reputation: 42674

I am assuming you know elisp.

There are a few approaches you can take:

  • Hook into auto-fill-mode. There are a lot of hard-coded conditionals there, so it might not work for you. You can potentially play with auto-fill-function and see if you have the hook you need there.

  • Make a character (probably .) "electric" so that when you press it, it inserts itself and then calls a function to determine how to fill the line you're on.

  • Set an after-change-hook to call a function that determines how to fill the sentence. This function will be called after every change to the buffer, so do it efficiently. (This mechanism is used by font-lock, so don't worry about it too much. It sounds slow, but really isn't -- people type slowly.)

Once you have hooked in at the right place, you just have to implement the filling logic. The source for sentence-at-point (from thingatpt) may be instructive.

Anyway, I've never heard of anyone doing this... but it is definitely possible. Like most things in Emacs, it's just a Simple Matter Of Programming.

Upvotes: 1

Jouni K. Sepp&#228;nen
Jouni K. Sepp&#228;nen

Reputation: 44128

If you put a comment marker at the end of each sentence, Emacs knows not to move the next line inside the comment:

chat chat chat.%
A new sentence
with goofed up wrapping that needs to be fixed.%
Mumble mumble%

Then M-q fills each sentence separately, at least in AUCTeX 11.85. (If you test this in Emacs, there seems to be a bug where if this is the first paragraph in the buffer and you type M-q, you get an error message. Just put a newline before the text to work around it.)

If you don't want to type the comment characters, you could take LaTeX-fill-paragraph and modify it so that sentence-ending punctuation at end of line works similarly to comments.

Upvotes: 4

Related Questions