目录

在company-mode补全过程中禁用orderless

orderless简介

orderless 包提供了一种新的补全风格,其特点是关键字之间顺序是无关的,比如输入内容“abc cde”和“cde abc”匹配到的候选项都是一样的,这便于我们在输入过程中刚好想到一个能降低候选项数量的关键字的时候,能直接在当前位置输入,而不是先移动光标到合适的位置后再输入。举个例子,我想找 company-mode支持哪些hook,并用 describe-variable 去看该hook变量的文档。

首先 C-h v 去调用 describe-variable ,输入关键字:“company hook”,有如下的候选项:

1
2
3
4
5
6
7
company-after-completion-hook
company-completion-finished-hook
company-completion-started-hook
company-mode-hook
company-search-mode-hook
global-company-mode-hook
company-completion-cancelled-hook

然后我想了下,我现在想看 company-completion-started-hook 的文档,于是我继续在当前位置输入“ st”,也就是最终我的输入内容为:“company hook st”,候选项缩减为1个了,可以直接选。这里的关键是我不用把光标移动到“hook”前后再去输入“st”,因为关键字顺序是无关的。你可能会说,很多补全框架都有这个功能,比如helm和ivy都有;对,不过orderless的好处在于它使用Emacs内置的completion-styles机制,所以有更广的适用范围。

起因

那么,为什么我要在company-mode补全过程中禁用orderless呢?

一开始我尝试orderless和company-mode一起使用,打算看下体验如何。由于在 company-mode补全过程中,如果敲空格会直接中断补全过程,故空格不能作为关键字分隔符了,所以我们需要添加不同于空格的额外的关键字分隔符,作者推荐的配置如下:

1
(setq orderless-component-separator "[ &]")

这里同时添加“ ”和“&”作为分隔符,这个倒是没有太大的问题。另一个问题就是补全过程中,候选项匹配的部分并没有被高亮,workaround是advice一下 company-capf--candidates ,下面同样是作者推荐的配置:

1
2
3
4
5
(defun just-one-face (fn &rest args)
  (let ((orderless-match-faces [completions-common-part]))
    (apply fn args)))

(advice-add 'company-capf--candidates :around #'just-one-face)

也就是让orderless使用completions-common-part作为高亮的face而已,不同于正常情况,这里由于company-mode写死了face,而且仅用一个face,故这里只能用一种face高亮。这个也还好,没什么问题。

主要问题是:

  1. 候选项的顺序不符合预期,比如我在emacs-lisp-mode的buffer中敲 (require ,我预期候选项 require 应该出现在第一个,结果它在中间(这个有解决方案,之后讲),我orderless设置了,如果输入内容的前缀是#,那么就按正则表达式风格去匹配,这时候你可能会想用 (#^require 表示要匹配以 require 开头的候选项,但是不行, company-mode不会把#^当成补全输入内容的一部分。
  2. 界面响应变得有一点点卡顿(还好,可接受的范围),出现这个问题主要是因为company-mode补全触发的比较频繁。

综上原因,加上company-mode本身设置一些选项,其内置的补全风格已经能满足我的要求,我决定还是在company-mode补全过程中禁用orderless。

如何实现在company-mode补全过程中禁用orderless?

一开始我尝试添加hook到 company-completion-started-hookcompany-after-completion-hook 中,在补全开始的时候修改 completion-styles 为Emacs的默认值,在补全结束的时候,把 completion-styles 修改回去,也就是只有orderless的状态。

效果是:补全过程中确实orderless没有起作用了,但是如果我最后敲了空格,这时候 company-after-completion-hook 触发, completion-styles 被还原成 (orderless) ,然后莫名其妙居然又开始补全了(似乎是以空格作为补全输入内容),导致我得按 C-g 来取消补全。

于是我看下了company-mode的源码,发现补全统一是由 company--perform 来触发的,于是决定advice它,在 company--perform 执行过程中,把 completion-styles 临时绑定为Emacs的默认值,核心配置如下(省略我对 orderless-style-dispatchersorderless-matching-styles 等的配置),添加了命令 osfva-toggle-enable-orderless-in-company ,用于启用/禁用 company-mode使用orderless :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
;; 此处配置省略...

;; 保存Emacs默认的completion-styles
(defvar osf-default-completion-styles
  (let ((sv (get 'completion-styles 'standard-value)))
    (and (consp sv)
         (condition-case nil
             (eval (car sv) t)
           (error completion-styles)))))
(setq completion-styles '(orderless basic)
      completion-category-overrides '((file (styles basic partial-completion))))

;; 此处配置省略...

(with-eval-after-load 'company
  (defvar osfva-enable-orderless-in-company nil)

  (defun osfva-toggle-enable-orderless-in-company ()
    (interactive)
    (setq osfva-enable-orderless-in-company
          (not osfva-enable-orderless-in-company)))

  (defun osfva-company-disable-orderless (orig-fun &rest args)
    "Diable orderless completion style when company is doing the completion."
    (let ((completion-styles (if osfva-enable-orderless-in-company
                                 completion-styles
                               osfva-default-completion-styles)))
      (apply orig-fun args)))
  (advice-add #'company--perform :around #'osfva-company-disable-orderless))

;; 此处配置省略...

如果不禁用orderless,如何解决候选项排序不符合预期的问题

如题,如果你不禁用orderless,那么首先你得加上作者推荐的配置,以修复company-mode 补全不高亮的问题以及空格不能作为分隔符的问题:

1
2
3
4
5
6
7
(setq orderless-component-separator "[ &]")

(defun just-one-face (fn &rest args)
  (let ((orderless-match-faces [completions-common-part]))
    (apply fn args)))

(advice-add 'company-capf--candidates :around #'just-one-face)

(注:不建议使用 (setq orderless-component-separator "[ &]") ,原因见下节的内容)。

接着,我们使用orderless的风格分发器(style dispatcher)机制来修复关键字不符合预期的问题(关于orderless风格的概念,请参考文档orderless,这里不再赘述),具体的,orderless有一个叫 orderless-style-dispatchers 的列表变量,该列表存储了一个个风格分发器(默认为 nil ,不进行额外的分发),每个风格分发器为接收三个参数的函数,参数的意义等下讲,在orderless决定要使用什么风格进行匹配之前,会按顺序调用 orderless-style-dispatchers 中的一个个风格分发器,由风格分发器决定要使用什么风格进行匹配,风格分发器返回的风格会覆盖默认风格(默认风格即 orderless-matching-styles 中的风格),风格分发器也可以返回 nil ,从而将风格交由列表中的下一个风格分发器决定,如果所有风格分发器都返回 nil ,则使用默认风格。

在说明风格分发器接收的三个参数的意义之前,先讲下orderless中component的概念,举个例子,在 orderless-component-separator 为默认值 " +" 的情况下,即一个或者多个空格的正则表达式时,输入关键字“company hook”,该关键字会被以空格作为分隔符,分割成两个component,分别是“company”以及“hook”,并且component是有下标概念的,从0开始,“company”的下标是0,“hook”的下标是1,以此类推。

好了, 现在可以讲风格分发器接收的三个参数了,这三个参数分别是 componentindextotal ,第一个参数 component ,顾名思义,就是分割出来的component字符串,第二个参数 index 是该component的下标,第一个 component的下标为0,第二个为1,以此类推,最后一个参数 total 则是component的总数量,比如在之前的例子中,分割出了两个component,这时候 total 就是2,注意到,风格分发器是针对每个component都会被调用一次,这意味着它可以为不同的component 指定不同的风格。前面说过了,一个风格分发器如果返回 nil 则代表它将风格交由 orderless-style-dispatchers 列表中的下一个风格分发器决定,还有其他的返回值形式,如下:

  1. 返回一个字符串,则将component的内容替换成该字符串,并且继续调用列表中下一个风格分发器以继续进行分发,不过下一个风格分发器接收到的component内容就是我们替换后的内容了。
  2. 返回单个风格(注:每个风格均为一个函数),或者返回一个非空风格列表,则返回的风格会代替默认风格。
  3. 返回一个cons cell,这种返回值基本相当于前两种返回值的合并,具体的,该cons cell的car指定了要替换的风格,同情况2一样,可以是单个风格,也可以是一个风格列表,除此之外,car还可以是nil,代表使用默认风格(这是情况2类型的返回值做不到的), cdr则为一个字符串,同情况1,用于替换component的内容。

有了以上知识后,我们便可以编写一些灵活的风格分发器了,比如我们可以根据component的前缀来决定其使用的风格,具体的,以“#”为前缀的component使用 orderless-regexp 风格,以“!”为前缀的component 使用 orderless-without-literal ,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(defvar osfva-orderless-prefix-dispatcher-alist
  `(("#" . orderless-regexp)
    ("!" . orderless-without-literal))
  "Determine which matching style to be used with given component prefix.
An element of this alist is of the form (PREFIX . MATCHING-STYLE) where
the PREFIX is the prefix of components, MATCHING-STYLE is the corresponding
matching style to be used.")

(defun osfva-orderless-prefix-dispatcher (component _index _total)
  "Different component prefix use different matching style.
See `osfva-orderless-prefix-dispatcher-alist'."
  (seq-some (lambda (prefix-style)
              (let ((prefix (car prefix-style))
                    (style (cdr prefix-style)))
                (when (string-prefix-p prefix component)
                  `(,style . ,(substring component 1)))))
            osfva-orderless-prefix-dispatcher-alist))

(setq orderless-style-dispatchers '(osfva-orderless-prefix-dispatcher))

上面的代码将风格分发器列表 orderless-style-dispatchers 设置成单个风格分发器 osfva-orderless-prefix-dispatcherosfva-orderless-prefix-dispatcher 根据component的前缀,如果前缀是“#”,则返回cons cell形式的返回值, car指定风格为 orderless-regexp ,cdr指定替换的component内容,这里我们去掉component的前缀部分,从而避免“#”作为补全的一部分,前缀“!”同理。

风格分发器可以有多个,我们可以再加一个风格分发器,解决我们一开始的问题,即company-mode中候选项排序不符合预期的问题,代码如下:

1
2
3
4
5
6
(defun osfva-company-first-initialism (_component index _total)
  (when (and (company--active-p) (= index 0))
    #'orderless-initialism))

(setq orderless-style-dispatchers '(osfva-company-first-initialism
                                    osfva-orderless-prefix-dispatcher))

上面的代码将风格分发器列表 orderless-style-dispatchers 设置成两个风格分发器, (osfva-company-first-initialism osfva-orderless-prefix-dispatcher) ,优先由 osfva-company-first-initialism 决定风格, osfva-company-first-initialism 决定不了风格,再由 osfva-orderless-prefix-dispatcher 决定风格, osfva-company-first-initialism 通过 (company--active-p) 判断 company-mode的补全是否处于激活状态(即补全的窗口有没有弹出),通过 (= index 0) 是否为第一个compoent,两个条件都满足时,返回风格 orderless-initialism (前面说过了,风格是函数,所以这里返回写 #'orderless-initialism ,写 'orderless-initialism 也一样,在Emacs Lisp中,两者等价),从而第一个component只会匹配候选项的前缀部分,针对非第一个component,则直接返回nil,将决定权交给下一个风格分发器,这就解决了我们一开始的问题。

还有一个问题,将“&”作为分隔符并不适用于所有模式

orderless作者推荐在和company-mode一起使用时,将分隔符设置成空格和“&”,即如下代码:

1
(setq orderless-component-separator "[ &]")

但是并不是所有模式中,“&”都能作为补全内容的一部分,比如在 c-modec++-mode 等中就不行,最好是让 orderless-component-separator 保持默认值 " +" ,然后在不同模式下使用不同的分隔符正则表达式,具体有两种方法,第一种是:为不同的模式添加hook, 在hook中 setq-local 设置特定于该buffer的分隔符正则表达式。第二种是:利用 orderless-component-separator 可以是函数的特性,将 orderless-component-separator 设置成自己定义的一个函数,在函数中判断当前模式(甚至加其他更复杂的判断),不同模式下返回不同的分隔符正则表达式。推荐后者,比较简洁,拓展性也比较强,可以加任意复杂的判断。

还剩一个比较大的问题,company-mode在后端company-capf起作用的情况下,内部会使用Emacs内置的completion-at-point机制,进而用到Emacs内置的completion-styles机制,这时候orderless才会起作用,如果起作用的后端不是company-capf,则orderless不会起作用,比如各语言关键字的补全经常由后端company-keywords来实现,此时 company-capf不起作用,进而orderless也不起作用。

结论

鉴于以上的种种问题,最终的建议是:在company-mode补全过程中禁用orderless。