Endless Parentheses

Ramblings on productivity and technical subjects.

profile for Malabarba on Stack Exchange

Longlines mode in LaTeX

Emacs.SE has truly revived my init file. Every day, a new snippet gets added. I started this series only 6 days ago, and it’s already on its fourth episode.

Today's question was asked by me, and answered by Tikhon Jelvis, Francesco, and Sacha Chua. As usual, this link might not work for you until the beta goes public.

Question

I’ve long been keeping my LaTeX documents under version control. Still, the effectiveness of this initiative would be greatly improved if I could bring myself to adopt a “one-sentence-per-line” approach. It would facilitate managing people’s conributions and reverting old changes.

If you’re wondering what’s so difficult about that, here is what such a document would look like. (Long sentences are to be avoided, but in scientific writing you don’t always have a choice).

A quite short sentence here.
New sentence on the same paragraph, with more text with more text with more text with more text.
Some more text, still on the same paragraph.
This is another very very very very very very very very very very very very very very very very very very very very long sentence.

The lack of readability above is evident. Having text extend beyond 80 columns is trouble enough, but intertwining long and short lines just makes my brain weep in despair.

The official solution is to activate visual-line-mode and add window margins (or just decrease window width). That causes the lines to wrap (visually, not in the file) and increases readability by an order of magnitude.

A quite short sentence here.
New sentence on the same paragraph, with more text with more
text with more text with more text.
Some more text, still on the same paragraph.
This is another very very very very very very very very very
very very very very very very very very very very very long
sentence.

But alas, it comes at a cost. If this text were indented by a few spaces (quite common in LaTeX), the outcome looks like it went through a blender. And don’t get me started on broken equations.

    A quite short sentence here.
    New sentence on the same paragraph, with more text with
more text with more text with more text.
    Some more text, still on the same paragraph.
    This is another very very very very very very very very
very very very very very very very very very very very very
long sentence.
    \begin{equation}
        H = equation * lines + sometimes - \need{to}{be / 
long}
    \end{equation}

Solution

tdsh points out in the comments, the adaptive-wrap package fixes the indentation issue for visual-line-mode.

The solution reached is a hack to make longlines-mode act more intelligently on LaTeX buffers. This minor mode is similar to the combination of visual-line-mode and window margins mentioned above. It makes lines wrap at fill-column, intead of window-width.

The advantage to longlines-mode is that it’s implemented in elisp (while window margins are done in C code), so we can hack it to our hearts’ content. Bafflingly, this little gold nugget has been marked obsolete on Emacs 24.4.

The hack below does 3 things:

  1. It enables “soft” wrapping of text. That is, the text is wrapped to fill-column in the buffer, but that does not reflect in the file.
  2. It fixes the indentation problem. So wrapping an indented line follows the line’s indentation. So environments with indented text (like itemize, or theorem) are much more readable.
  3. It even prevents wrapping of equations!

The code is a bit heavy, so I’ve added comments describing the lines which were actually changed.

(require 'longlines nil t)
(add-hook 'LaTeX-mode-hook #'longlines-mode)

(defun longlines-encode-region (beg end &optional _buffer)
  "Replace each soft newline between BEG and END with exactly one space.
Hard newlines are left intact. The optional argument BUFFER exists for
compatibility with `format-alist', and is ignored."
  (save-excursion
    (let ((reg-max (max beg end))
          (mod (buffer-modified-p)))
      (goto-char (min beg end))
      ;; Changed this line to "swallow" indendation when decoding.
      (while (search-forward-regexp " *\\(\n\\) *" reg-max t)
        (let ((pos (match-beginning 1)))
          (unless (get-text-property pos 'hard)            
            (goto-char (match-end 0))   ; This line too
            (insert-and-inherit " ")
            (replace-match "" :fixedcase :literal) ; This line too
            (remove-text-properties pos (1+ pos) 'hard))))
      (set-buffer-modified-p mod)
      end)))

(defun longlines-wrap-line ()
  "If the current line needs to be wrapped, wrap it and return nil.
If wrapping is performed, point remains on the line. If the line does
not need to be wrapped, move point to the next line and return t."
  (if (and (bound-and-true-p latex-extra-mode)
           (null (latex/do-auto-fill-p)))
      (progn (forward-line 1) t)
    ;; The conditional above was added for latex equations. It relies
    ;; on the latex-extra package (on Melpa).
    (if (and (longlines-set-breakpoint)
             ;; Make sure we don't break comments.
             (null (nth 4 (parse-partial-sexp
                           (line-beginning-position) (point)))))
        (progn
          ;; This `let' and the `when' below add indentation to the
          ;; wrapped line.
          (let ((indent (save-excursion (back-to-indentation)
                                        (current-column))))
            (insert-before-markers-and-inherit ?\n)
            (backward-char 1)
            (delete-char -1)
            (forward-char 1)
            (when (> indent 0)
              (save-excursion
                (insert (make-string indent ? )))
              (setq longlines-wrap-point
                    (+ longlines-wrap-point indent))))
          nil)
      (if (longlines-merge-lines-p)
          (progn (end-of-line)
                 (if (or (prog1 (bolp) (forward-char 1)) (eolp))
                     (progn
                       (delete-char -1)
                       (if (> longlines-wrap-point (point))
                           (setq longlines-wrap-point
                                 (1- longlines-wrap-point))))
                   (insert-before-markers-and-inherit ?\s)
                   (backward-char 1)
                   (delete-char -1)
                   (forward-char 1)
                   ;; This removes whitespace added for indentation.
                   (while (eq (char-after) ? )
                     (delete-char 1)
                     (setq longlines-wrap-point
                           (1- longlines-wrap-point))))
                 nil)
        (forward-line 1)
        t))))

Update <2014-10-07 Tue>

Fixed handling of comments. Now comments don’t get wrapped either.

comments powered by Disqus