multi lines python indentation on emacs

2020-05-29 10:17发布

问题:

Im an emacs newbie, I want emacs to be able to indent my code like this

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

It's not possible to do this automatically by default (without manually inserting spaces or C-c >), since emacs always indents 4 spaces (unless Im splitting multiple arguments over multiple lines).

Whats the best approach to do this?

PS: If this is a bad idea (against PEP 8 or something) please do tell me

回答1:

I agree with Aaron about the desirability of your stylistic choice, but since I also agree with him that Emacs Lisp is fun, I'll describe how you might go about implementing this.

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 (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. Coming from a Python background you might well ask, "why not do this decorator-style?" The Emacs advice mechanism is designed (1) to keep advice well separated from the original; and (2) to have multiple pieces of advice for a single function that don't need to co-operate; (3) to allow you individual control over which pieces of advice are turned on and off. You could certainly imagine writing something similar in Python.

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 certain circumstances, so we better catch that error:

(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 best way to do 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!



回答2:

That's pretty ugly and would require you to write some emacs lisp. I need to learn emacs lisp so if it wasn't so ugly, I would probably be up for doing it. But it is and I'm not. Looks like you get to learn emacs lisp :) (if you actually want to do this). I'm sort of jealous. At any rate, you said that informing you that this is a bad idea was an acceptable answer so here goes:

That's a terrible stylistic choice. Isn't

egg = spam.foo('viviviv')
egg = egg.foo('emacs', 'emacs', 'emacs')

easier to read?

While not specifically against PEP 8, it is mentioned that use of the line continuation character should be kept to a minimum. Also, this most definitively and objectively goes against the spirit of PEP 8. I'm just not sure how ;)