在Emacs中Python多行缩进

2 投票
2 回答
2645 浏览
提问于 2025-04-16 06:18

我刚开始学用emacs,想让emacs能像这样自动缩进我的代码

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

默认情况下,这个功能是不能自动实现的(也就是说不能不手动插入空格或者按C-c >),因为emacs总是缩进4个空格(除非我把多个参数分到多行上)。

有什么好的方法可以做到这一点吗?

PS: 如果这个主意不好(比如违反了PEP 8之类的规范),请告诉我。

2 个回答

3

这看起来挺糟糕的,而且你需要写一些emacs lisp代码。我需要学习emacs lisp,如果它不是这么丑的话,我可能会愿意去做。但它就是这样,所以我不想。看起来你得去学emacs lisp了 :)(如果你真的想做这个的话)。我有点嫉妒。无论如何,你说告诉你这是个坏主意是可以接受的答案,所以我就说说:

这真是个糟糕的风格选择。难道

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

不是更容易阅读吗?

虽然这并不完全违反PEP 8的规定,但提到应该尽量少用换行符。此外,这绝对是违背了PEP 8的精神。我只是有点不确定怎么说;)

25

我同意Aaron关于你风格选择的看法,但因为我也同意他认为Emacs Lisp很有趣,所以我来描述一下你可以如何实现这个功能。

Emacs的python-mode有一个函数叫python-calculate-indentation,它负责计算一行的缩进。而处理续行的相关部分深藏在这个函数里面,没有简单的方法可以配置它。

所以我们有两个选择:

  1. 用我们自己的版本完全替换python-calculate-indentation(每次python-mode更新时都得维护,真是个噩梦);或者
  2. python-calculate-indentation进行“建议”:也就是说,把它包裹在我们自己的函数里,处理我们感兴趣的情况,其余的则交给原来的函数。

在这种情况下,选择(2)似乎是可行的。所以我们就这样做吧!第一步是阅读关于建议的手册,它建议我们的建议应该像这样:

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

这里的ad-do-it是一个魔法标记,defadvice会用原始函数替换它。你可能会问:“为什么不使用装饰器风格呢?”Emacs的建议机制设计是为了(1)将建议与原始函数很好地分开;(2)允许一个函数有多个不需要合作的建议;(3)让你可以单独控制哪些建议是开启或关闭的。你当然可以想象在Python中写出类似的东西。

下面是如何判断当前行是否是点状续行:

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

这里有一个问题:调用beginning-of-line实际上会把光标移动到行的开头。哎呀。我们在计算缩进时不想移动光标。所以我们最好把这个调用包裹在save-excursion中,以确保光标不会乱跑。

我们可以通过向后跳过标记或括号表达式(Lisp称之为“S表达式”或“sexps”)来找到需要对齐的点,直到找到点或者到达语句的开头。一个好的Emacs习惯是在缓冲区的限制部分进行搜索,可以通过缩小缓冲区,只包含我们想要的部分:

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

然后继续向后跳过sexps,直到找到点,或者backward-sexp停止进展:

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

这里的ad-return-value是一个魔法变量,defadvice用它来获取被建议函数的返回值。虽然看起来有点丑,但很实用。

现在这里有两个问题。第一个是backward-sexp在某些情况下可能会报错,所以我们最好捕获这个错误:

(ignore-errors (backward-sexp))

另一个问题是如何退出循环并指示成功。我们可以通过声明一个命名的block来同时做到这两点,然后调用return-from块和退出是Common Lisp的特性,所以我们需要(require 'cl)

让我们把所有的内容整合在一起:

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

我不会说这是最好的做法,但它展示了一些中等复杂的Emacs和Lisp特性:建议远足缩小跳过sexps错误处理块和退出。祝你玩得开心!

撰写回答