目录

解决同时使用cider和evil-mode时,company-mode的按键绑定在补全过程中失效问题

背景

这是我很久之前遇到的问题,我同时使用evil-mode和cider,用company-mode进行补全,结果碰到了一个奇怪的现象:在cider作用的buffer中(即cider的REPL buffer或者clojure-mode buffer等)中用company-mode进行补全, company-mode的按键绑定会失效,比如我在 company-active-map 中绑定 C-j/C-k分别去选择下一个/上一个候选项,结果在cider的buffer中全部失效,变成触发了evil-mode的按键绑定。

当时在cider的仓库发了Issue:

Key bindings of company-mode don’t work while evil-mode is enabled

很久没有人回复,作者自己也不用evil-mode,估计没有兴趣去管这个Issue,我只好自己去寻找解决方案,利用各种调试手段在company-mode和cider的源码中到处看问题所在,最终找到了问题根源,想出来了一个挺简洁的workaround,当时看doom-emacs用户也有遇到这个问题,于是把workaround发在了doom-emacs的那个Issue里面:

Cider + Company not working as it should

现在整理成一个文章,相当于翻译吧。

问题所在

问题在于cider会频繁执行一个叫 nrepl-bdecode 的函数,它会不停把一个叫 “nrepl-decoding” buffer的major-mode改成 fundamental-mode (不懂为什么要这么做,因为这个buffer是 get-buffer-create 创建的,Info文档中提到了,该函数创建的buffer一开始就会处于 fundamental-mode )。把 major-mode改成 fundamental-mode 会触发 after-change-major-mode-hook ,evil-mode在这个hook触发的时候,会去提升它keymap的优先级,提升方法就是:把 evil-mode-map-alist 移到 emulation-mode-map-alists 的最前面(evil-mode的模式编辑以及它自创的各个keymap优先级层次是在 emulation-mode-map-alists 这层模拟的)。

company-mode的 company-active-map 也是在 emulation-mode-map-alists 这一层起作用的,具体的,在每次补全开始的时候,company-mode也会像 evil-mode一样,把自己的 company-emulation-alist 放到 emulation-mode-map-alists 的最前面,其中, company-emulation-alist 的值是 ((t . company-my-keymap)) ,而 company-my-keymap 的值会被设置成 company-active-map ,这样,便确保了 company-active-map 有较高的优先级,然后在补全结束的时候,company-mode会设置 company-my-keymap 为nil,不指向 company-my-keymap ,从而使 company-active-map 中的按键绑定失效。总结来说, company-active-map 会在company-mode补全开始的时候生效且有比较高的优先级,会在company-mode补全结束的时候失效,从而不会影响到其他按键绑定。

正常情况下,在company-mode补全过程中,不会有代码去改变major-mode,一般只有在用户打开新的文件,切换buffer等等才会有代码去切换major-mode,这时候才会触发上面的操作。而触发company-mode的补全时,它会提升自己的keymap 到 emulation-mode-map-alists 的最前面,从而在优先级上高于evil-mode的 keymap。但是现在由于cider不停修改"nrepl-decoding"这个buffer的 major-mode,不停触发 after-change-major-mode-hook ,导致evil-mode不停把它的 evil-mode-map-alist 放到 emulation-mode-map-alists 的最前面,于是出现在company-mode补全过程中,evil-mode把自己的keymap优先级提升到比company-mode的keymap还高的情况,从而导致company-mode的按键绑定失效的问题。

workaround

利用“在company-mode补全时, company-my-keymap 会指向 company-active-map ,而在补全结束后它的值会变成nil,即 company-active-map 会在company-mode补全开始的时候生效,会在 company-mode补全结束的时候失效,从而不会影响到其他按键绑定”这点,我们可以给出一个很简单的workaround:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(add-hook 'evil-local-mode-hook
            (lambda ()
              ;; Note:
              ;; Check if `company-emulation-alist' is in
              ;; `emulation-mode-map-alists', if true, call
              ;; `company-ensure-emulation-alist' to ensure
              ;; `company-emulation-alist' is the first item of
              ;; `emulation-mode-map-alists', thus has a higher
              ;; priority than keymaps of evil-mode.
              ;; We raise the priority of company-mode keymaps
              ;; unconditionally even when completion is not
              ;; activated. This should not cause problems,
              ;; because when completion is activated, the value of
              ;; `company-emulation-alist' is ((t . company-my-keymap)),
              ;; when completion is not activated, the value is ((t . nil)).
              (when (memq 'company-emulation-alist emulation-mode-map-alists)
                (company-ensure-emulation-alist))))

即在 evil-local-mode 启用的时候,强制把 company-emulation-alist 放到 emulation-mode-map-alists 的最前面(这是 company-ensure-emulation-alist 做的,company-mode自己的函数),由于它的keymap只在补全时生效,所以无条件放到列表最前面是不要紧的。