31

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

  • Soft wrapping of text (say using the longlines and (set long-lines-auto-wrap 't)). This is because I don't want to impose requirements on my collaborators' editors, and I sometimes use other unix tools to examine these files.

I think what I do want is

  • For fill-paragraph to fill between newlines that look like they mark the end of a sentence.
  • A solution that works with auto-fill-mode would be a bonus.

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.

Community
  • 1
  • 1
dmckee --- ex-moderator kitten
  • 98,632
  • 24
  • 142
  • 234
  • 1
    It should be possible to just do the equivalent of M-^ (`delete-indentation`) on the beginning of each line that does not start a sentence. But why don't you want soft wrapping? – ShreevatsaR Feb 12 '09 at 04:29
  • "why don't you want soft wrapping?" Hmmm. I don't know, really. It's bugged me for a long time. Why _do_ I want soft wrapping? – dmckee --- ex-moderator kitten Feb 12 '09 at 04:37
  • Do you think it would be worth generalizing this question so people can give solutions for other editors besides emacs? It's also more general than LaTeX, come to think of it. All the reasons for doing this apply to HTML documents as well, for example. – dreeves Feb 12 '09 at 20:17
  • I went ahead and added the more general question. I'm happy to delete it if you want to fold it in with this one. – dreeves Feb 12 '09 at 21:30
  • It would have been reasonable, indeed, but real life intervened and I was away from my computer... So, I linked to your version. – dmckee --- ex-moderator kitten Feb 12 '09 at 22:03

10 Answers10

10

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)))
Chris Conway
  • 55,321
  • 43
  • 129
  • 155
5

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)
Ivan Andrus
  • 5,221
  • 24
  • 31
  • I'm sorry to say that I've been very slow to respond to new answers on this questions; I've had other things at the top of my work queue. This code works very nicely indeed when I'm typing new text---it is exactly what I asked for. It's not handling existing text with sentences starting in mid line on `fill-paragraph`, but does handle them if I type a space at the start of each sentence. Thanks. – dmckee --- ex-moderator kitten Feb 26 '12 at 03:32
  • 1
    Do you mean that it thinks sentences should be separated by double spaces? That can be changed by setting `sentence-end-double-space`. If you mean that you want it to break unconditionally after a `.`, `!`, `?` etc. then you could change the value of `sentence-end`, or change the call to `sentence-end` to whatever regexp you prefer. – Ivan Andrus Feb 26 '12 at 19:24
  • 1
    For some reason this solution still wraps long lines for me - it does however add a newline after the period. How can I make it not wrap within a single sentence? – icks Feb 06 '14 at 12:18
  • @icks Hmm. This isn't the same as I have in my .emacs, so they must have diverged at some point. Try changing the `(do-auto-fill)` to `nil` and see if that helps. – Ivan Andrus Feb 07 '14 at 05:39
4

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.

Jouni K. Seppänen
  • 43,139
  • 5
  • 71
  • 100
3
(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)
Rob
  • 31
  • 1
  • For my personal use I removed the `(insert "\n")`. Thanks for that! This is the best solution for me so far. – yogsototh Jul 07 '19 at 07:00
2

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).

scottfrazer
  • 17,079
  • 4
  • 51
  • 49
1

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.

Community
  • 1
  • 1
pnj
  • 1,349
  • 1
  • 11
  • 14
1

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).

agarttha
  • 11
  • 2
1

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.

jrockway
  • 42,082
  • 9
  • 61
  • 86
1

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
dreeves
  • 26,430
  • 45
  • 154
  • 229
1

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.

C.K.
  • 66
  • 2