Indentation advice

,

Over on Stack Overflow, kusut asked,

I’m an Emacs newbie. I want Emacs to be able to indent my [Python] code like this:

egg = spam.foooooo('vivivivivivivivivi') \
          .foooooo('emacs', 'emacs', 'emacs', 'emacs')

In other words, with the dot at the start of the the continuation line lining up vertically with the dot on the first line. Emacs’ built-in python-mode at the time of writing just indents the second line by python-indent spaces, resulting in:

egg = spam.foooooo('vivivivivivivivivi') \
    .foooooo('emacs', 'emacs', 'emacs', 'emacs')

The simple and obviously correct answer is the one given by Aaron Sterling, to the effect that the Python style guide says,

The preferred way of wrapping long lines is by using Python’s implied line continuation inside parentheses, brackets and braces.

and it’s probably not a good idea to depend on backslashes for line continuation, because one day you’ll be reformatting some long expression and either you’ll forget to remove a no-longer-necessary backslash, leading to

SyntaxError: unexpected character after line continuation character

or worse, forget to insert a necessary backslash, silently changing the meaning of your code.

Aaron’s answer is simple and correct, but it sweeps an interesting problem under the rug of doing the right thing. For supposing, just supposing, that you actually wanted to do this in Emacs, how would you go about it? And what interesting features of Emacs Lisp might you learn in the process? So, just for amusement, here’s how.

Emacs’ python-mode computes the indentation of a line in the function python-calculate-indentation, and the relevant section for handling continuation lines is buried deep inside the function, with no easy way to configure it. So we have two options:

  1. Replace the whole of python-calculate-indentation with our own version, leading to a maintenance nightmare whenever python-mode changes; or

  2. Advise” the function python-calculate-indentation: that is, wrap it in our own function that handles the case we’re interested in, and otherwise defers to the original.

Option (2) seems just about doable in this case. So let’s go for it! The first thing to do is to read the manual on advice which suggests that our advice should look like this:

(defadvice python-calculate-indentation (around continuation-with-dot)
  "Handle continuation lines that start with a dot and try to
line them up with a dot in the line they continue from."
  (unless 
      (this-line-is-a-dotted-continuation-line) ; TODO
    ad-do-it))

Here ad-do-it is a magic token that defadvice substitutes with the original function. If you’re coming from a Python background, you might well ask, “why not do this decorator-style?” Well, the Emacs advice mechanism is designed (1) to keep advice well separated from the original; (2) to support multiple pieces of advice for a single function that don’t need to co-operate (much); and (3) to allow to turn on and off individual pieces of advice. You could certainly imagine writing something similar in Python, but it’d be more complicated than a bare decorator.

Here’s how to tell if the current line is a dotted continuation line:

(beginning-of-line)
(when (and (python-continuation-line-p)
           (looking-at "\\s-*\\."))
    ;; Yup, it's a dotted continuation line. TODO
    ...)

There’s one problem with this: that call to beginning-of-line actually moves point to the beginning of the line. Oops. We don’t want to move point around when merely calculating indention. So we better wrap this up in a call to save-excursion to make sure that point doesn’t go a-wandering.

We can find the dot that we need to line up with by skipping backwards over tokens or parenthesized expressions (what Lisp calls “S-expressions” or “sexps”) until either we find the dot, or else we get to the start of the statement. A good Emacs idiom for doing a search in a restricted part of the buffer is to narrow the buffer to contain just the part we want:

(narrow-to-region (point)
                  (save-excursion
                    (end-of-line -1)
                    (python-beginning-of-statement)
                    (point)))

and then keep skipping sexps backwards until we find the dot, or until backward-sexp stops making progress:

(let ((p -1))
  (while (/= p (point))
    (setq p (point))
    (when (looking-back "\\.")
      ;; Found the dot to line up with.
      (setq ad-return-value (1- (current-column)))
      ;; Stop searching backward and report success (TODO)
      ...)
    (backward-sexp)))

Here ad-return-value is a magic variable that defadvice uses for the return value from the advised function. Ugly but practical.

Now there are two problems with this. The first is that backward-sexp can signal an error in some circumstances, so we better catch those errors:

(ignore-errors (backward-sexp))

The other problem is that of breaking out of the loop and also indicating success. We can do both at once by declaring a named block and then calling return-from. Blocks and exits are Common Lisp features so we’ll need to (require 'cl). Let’s put it all together:

(require 'cl)

(defadvice python-calculate-indentation (around continuation-with-dot)
  "Handle continuation lines that start with a dot and try to
line them up with a dot in the line they continue from."
  (unless 
      (block 'found-dot
        (save-excursion
          (beginning-of-line)
          (when (and (python-continuation-line-p)
                     (looking-at "\\s-*\\."))
            (save-restriction
              ;; Handle dotted continuation line.
              (narrow-to-region (point)
                                (save-excursion
                                  (end-of-line -1)
                                  (python-beginning-of-statement)
                                  (point)))
              ;; Move backwards until we find a dot or can't move backwards
              ;; any more (e.g. because we hit a containing bracket)
              (let ((p -1))
                (while (/= p (point))
                  (setq p (point))
                  (when (looking-back "\\.")
                    (setq ad-return-value (1- (current-column)))
                    (return-from 'found-dot t))
                  (ignore-errors (backward-sexp))))))))
    ;; Use original indentation.
    ad-do-it))

(ad-activate 'python-calculate-indentation)

I won’t claim that this is the only or the best way to do something like this, but it illustrates a bunch of moderately tricky Emacs and Lisp features: advice, excursions, narrowing, moving over sexps, error handling, blocks and exits. Enjoy!