Emacs Lisp 模式下 If 缩进出问题

大概一周前就遇到过,今天第二次:我的 Emacs 用过一段时间 if 的缩进出了问题:

(if a
    b
    c)

正确缩进是:

(if a
    b
  c)

Emacs -Q 试下没问题,应该是我配置问题,但简单搜索了下 lisp-indent-function,没发现我的配置和第三方包有修改 if 的缩进。我跑到 lisp-mode.el 下重现执行了一遍

(put 'if 'lisp-indent-function 2)

然后缩进就正常了,不知道谁在什么时候把这个属性给删掉了,大家有没有类似经历,或者 Debug 的思路。

看看有没有用 aggressive-indent ? 这个自动化缩进的插件很有问题,用了很久以后现在全部禁用掉了。

建议配置文件二分查找法重启做对比测试来定位问题。

没安装这个。

还没发现稳定重现的方法,不是一开启 Emacs 就会有这个问题,不太好正向(从配置)调查,等第三次出现这个问题再调查,感觉先了解出现这个现象的哪些可能会有所帮助。

Common Lisp 的 if 缩进标准就是

(if a
    b
    c)

lisp-mode 默认的和 Emacs Lisp 一样反而是错的

见最下面一段。

https://www.emacswiki.org/emacs/IndentingLisp


另一些关于 CL indent 的 trivia

http://dept-info.labri.fr/~strandh/Teaching/PFS/Common/Strandh-Tutorial/indentation.html

cl的if是

(IF COND THEN-FORM &optional ELSE-FORM)

Elisp的if是

(IF COND THEN-FORM &rest ELSE-FORM)

elisp的else-form是被progn隐式包裹的,所以和CL的缩进方法不同


抓了几个lisp方言来试,Common Lisp, Scheme, Clojure是optional else-form,elisp和hy是rest else-form

1 个赞

是不是设置过 lisp-indent-function 变量?

(let ((lisp-indent-function 'common-lisp-indent-function))
  (with-current-buffer (generate-new-buffer "test-if.el")
    (erase-buffer)
    (insert "(if t
foo
bar)")
    (lisp-mode)
    (pp-buffer)
    (buffer-substring-no-properties (point-min) (point-max))))
;; => 
;; "(if t
;;     foo
;;     bar)
;; "
(let ((lisp-indent-function 'lisp-indent-function))
  (with-current-buffer (generate-new-buffer "test-if.el")
    (erase-buffer)
    (insert "(if t
foo
bar)")
    (lisp-mode)
    (pp-buffer)
    (buffer-substring-no-properties (point-min) (point-max))))
;; =>
;; "(if t
;;     foo
;;   bar)
;; "
2 个赞

之前搜索过 lisp-indent-function,没发现有改过它。

这个问题没法重现,如果遇到第三次我再调查。

你用了 slime/sly 的话是会自动改的。

找到根源了,缩进被 newlisp-mode 给修改了:

我安装了这个包,但压根就没用,之所以 Emacs 依旧执行了这个文件,貌似是 Emacs 26.1 新加入的这个特性的锅:

** New var 'definition-prefixes' is a hash table mapping prefixes to
the files where corresponding definitions can be found.  This can be
used to fetch definitions that are not yet loaded, for example for
'C-h f'.

所以解决方法就是把这个包给删了,看样子安装了一个包,就算不用,Emacs 也会可能加载,通过 C-h f 补全自行加载。:slightly_frowning_face:

一出现这个问题,我立马用 C-h v load-history 查看那些文件加载了,从上往下一个一个文件搜索 lisp-indent-function,然后就发现了 newlisp-mode.el。

3 个赞

用了下common-lisp-indent-function大部分时间可以正确handle,不过偶尔会抽风挺不爽的,缩进cl-flet的时候比普通lisp-indent-function好看

common-lisp-indent-function

(cl-flet ((update-db! ()
              ;; The copy is necessary because our SQL query action
              ;; may conflicts with running Firefox.
              (copy-file counsel-ffdata-database-path
                         counsel-ffdata--temp-db-path)
              (clrhash counsel-ffdata--cache)))
    (let* ((path counsel-ffdata--temp-db-path))
      (condition-case e
          (if (file-exists-p path)
              (when force-update?
                (delete-file path)
                (update-db!))
            (update-db!))
        (error "Failed to ensure firefox database: %s" e))
      nil))

lisp-indent-function

(cl-flet ((update-db! ()
                        ;; The copy is necessary because our SQL query action
                        ;; may conflicts with running Firefox.
                        (copy-file counsel-ffdata-database-path
                                   counsel-ffdata--temp-db-path)
                        (clrhash counsel-ffdata--cache)))
          (let* ((path counsel-ffdata--temp-db-path))
            (condition-case e
                (if (file-exists-p path)
                    (when force-update?
                      (delete-file path)
                      (update-db!))
                  (update-db!))
              (error "Failed to ensure firefox database: %s" e))
            nil))

对,但是我懒得折腾,而且也不是所有人都很讲究,缩进不一样需要协作时就会难受。

我自己一般取个折中写法:

(cl-flet ((mean
           (numbers)
           (/ (apply #'+ numbers)
              (float (length numbers)))))
  (mean '(1 2 3 4)))

总的来说还是利大于弊

(cl-defun func (&whole
                  whole-args
                  normal args
                &optional
                  optionA
                &key
                  keyA
                  keyB
                &allow-other-keys
                &rest
                  rest-part))

lisp-indent-function没法漂亮缩进带上CL扩展的参数列表

git hooks强制就好了,哈哈

common-lisp-indent-function 的确大部分时候表现良好,但我已经忘了为何我用了一段时间之后放弃了。

我现在回到 lisp-indent-function,但是针对 plist 做了些改进。

以前:

;; '(:a 1
;;      :b 2
;;      :c 3)

现在:

;; '(:a 1
;;   :b 2
;;   :c 3)

冒号不对齐太难受了。

lisp-indent-function写handler只有两个参数:发生缩进的位置(一般是行首)parse-partial-sexp的返回值,不是人用的。

lisp-indent-function 最难受的是不能很好识别和缩进带keyword的参数列表,比如

(propertize icon 'face `(:inherit ,face
                         :height 1.1)

默认显示为

(propertize icon 'face `(:inherit ,face
                                  :height 1.1)

最后找了个改装版本才搞定。

我现在用的 indent 函数是抄来的,能偷懒则偷懒:https://github.com/Fuco1/.emacs.d/blob/af82072196564fa57726bdbabf97f1d35c43b7f7/site-lisp/redef.el#L12-L94

parse-partial-sexp 我需要把 docstring 贴在旁边作为注释,或者:

(let* ((st (parse-partial-sexp (point-min) (point)))
       (depth              (nth 0 st))
       (start-of-innermost (nth 1 st))
       (start-of-last-sexp (nth 2 st))
       (inside-a-string?   (nth 3 st))
       ...
       ))

不如直接用模式匹配

(pcase (parse-partial-sexp)
  (`(,depth ,start-of-inner-most ,start-of-last-sexp ,inside-str? . ,rest)))

我现在也用的这个改装版,挺好。

就你这个例子,可以转而用 list 绕过这个问题(如果非要换行的话,我一定会这么干)。