跳过正文
Background Image

Emacs 中的补全

·1899 字·4 分钟·
lizqwerscott
作者
lizqwerscott

Emacs 中的补全有很多种形式,写代码的时候弹出来的补全框,AI 使用 inline text 实现的续写,使用 M-x 调起来的 minibuffer 补全,某些交互式命令调用时弹出的补全框,都是补全。

但是不管那种补全,都由以下模块组成:

补全后端框架
#

补全后端负责收集和整理数据,为前端提供统一的补全接口。

minibuffer 中的补全
#

补全项的来源都是命令自己产生的数据。

普通 Buffer 中的补全
#

capf
#

这是一个 Emacs 自己构建的系统,前端调用当前 buffer 的 completion-at-point-functions 这个变量里面的函数,来获取补全,可以使用 cape 来增强 capf 的能力。

第三方
#

  • company

    company 自己实现的补全系统,只能给 Company 前端自己使用,或者也可以使用 cape 提供的 cape-company-to-capf 来转化成 capf 的接口。

  • acm

    lsp-bridge 自己构建的补全系统,只能给 acm 前端使用,但是能调用 capf 的接口来提供数据。

补全前端框架
#

有了数据后,需要考虑如何呈现给用户。Emacs 提供了多种前端界面可供选择。

minibuffer 中的补全
#

使用 Emacs 原生补全接口
#

  • Ido

    Ido 包允许您以最少的按键次数切换缓冲区并访问文件和目录。它是 Stephen Eglen 开发的交互式缓冲区切换包 Iswitchb 的超集,给默认切换 buffer 和打开文件提供了更加强大的功能,显示补全项的方式是直接在输入的后面使用 { a | b } 来显示。 但是默认不支持 M-x 的补全显示,需要安装 smex 来使用 ido 风格的补全显示方式。

  • Icomplete

    是 Emacs 原生的 minibuffer 补全方式。

  • Fido (Fake ido)

    Fido 模式是 Icomplete 模式的替代方案。它与 Icomplete 模式非常相似,但保留了来自名为 Ido 模式的流行扩展的一些功能。(默认使用 flex 风格 来匹配)

  • icomplete-vertico-mode

    和 icomplete 是一样的,但是垂直展示补全项。

  • fido-vertical-mode

    和 fido 是一样的,但是垂直展示补全项。

  • mct

    使用原生 *Completions* buffer 来弹出补全项,但是能自动更新 *Completions* buffer, 为原生补全提供更多的智能化。

  • vertico

    目前最新的框架,可以支持前端样式,能根据补全的内容自定义补全样式。 能支持同时多种类别补全项目展示,根据补全项的分类来分隔展示。

第三方
#

  • helm

    主要不使用 minibuffer 来进行补全,而是使用单独的 buffer 来进行补全项的展示。

普通 Buffer 中的补全
#

使用 Emacs capf 作为后端
#

  • 使用 *Completion*

    使用一个单独的 buffer 来展示补全项。

  • Icomplete

    设置变量 icomplete-in-buffer 为 t 启用(可能需要开关 icomplete-mode ),默认使用 icomplete-mode 的补全风格,在输入的后面显示补全项。

  • completion-preview-mode

    使用 inline Text 的方式展示预览项,默认只展示一个。

  • corfu

    比较轻量的补全前端,使用 posframe 的方式弹出补全菜单,界面比较现代。

第三方
#

  • company

    使用 overlay 来弹出补全菜单。

匹配,过滤和排序
#

补全样式
#

在补全过程中,补全项会根据输入内容匹配相应条目。Emacs 默认配置了多种补全样式

补全样式的确定流程如下:

  1. 检查 completion-category-overrides 中是否有当前补全项的分类,若有则使用;
  2. 若无,检查 completion-category-defaults
  3. 若两者均无或补全项无分类,则使用默认的 completion-styles

确定样式列表后,Emacs 按列表顺序尝试补全:若首个样式返回 nil ,则依次使用后续样式,形成 降级回退 机制。

匹配和过滤
#

orderless
#

它提供了一个强大的匹配和过滤机制,一下是来自 orderless 官方的介绍。

它将模式分割成以空格分隔的组件,并匹配所有组件(顺序不限)的候选词。每个组件可以通过多种方式匹配:字面匹配、正则表达式匹配、首字母缩写匹配、灵活匹配或多个单词前缀匹配。默认情况下,正则表达式匹配和字面匹配已启用。

并且它还提供了一个很强大的功能, Style dispatchers, 可以支持将带有前缀的输入内容,单独使用其他匹配规则。

基与这个功能和 pyim 提供的词库,就可以简单实现一个输入对应前缀,然后输入拼音,就可以直接匹配和过滤中文。1

(require 'pyim)

(defun chinese-orderless-regexp (component)
  "Match COMPONENT as a chinese regexp."
  (condition-case nil
      (pyim-cregexp-build
       (progn (string-match-p component "")
              component))
    (invalid-regexp nil)))

(with-eval-after-load 'orderless
  (add-to-list 'orderless-affix-dispatch-alist
               `(?= . ,#'chinese-orderless-regexp)))

orderless-affix-dispatch-alist 这个变量里面保存的就是 orderless 用来检测输入的前缀对应的匹配函数。

fussy
#

这个包提供了一个给匹配项排序的功能,通过评分算法计算出匹配项的评分,然后根据评分来进行排序。并且可以自己定义评分和匹配过滤函数。

这是一个为 Emacs 提供 completion-style 包,能够利用 flx 以及各种其他库,例如 fzf-native 提供智能评分和排序。

匹配函数可利用 orderless 的过滤功能,但默认评分函数不支持其前缀特性,导致匹配项得分均为零或负数。

为此,我自定义了支持前缀评分的函数,完整实现详见配置中的 fussy-orderless 文件。

主要核心内容就是给每一个 orderless 的前缀添加评分,最后计算总共的评分,将各个评分相加起来。

例如,以下代码用于计算中文内容的评分: 通过评估匹配内容在整体中的占比及其位置信息来计算得分2

(defun fussy-orderless-chinese-regexp-score (string query)
  "Use QUERY and STRING calc chinese regexp score."
  (require 'pyim)
  (when-let* (((string-match-p "\\cc" string))
              (regexp (when (fboundp 'pyim-cregexp-build)
                        (pyim-cregexp-build query))))
    (string-match regexp string)
    (pcase-let* ((`(,start ,end) (match-data))
                 (len (length string)))
      (when (< end len)
        (list (+ (* 20 (/ (float (- end start))
                          len))
                 (* 80 (/ (float (- len start)) len)))
              start
              end)))))

之后只要修改以下内容:

首先需要将想要使用的计分函数添加到 fussy-whitespace-ok-fns 变量中,以确保传递给评分函数的查询内容中的空格不被去除,避免 orderless 无法识别前缀。

(with-eval-after-load 'fussy
  (add-to-list 'fussy-whitespace-ok-fns
               #'fussy-orderless-score-with-flx-rs))

然后就是将过滤函数设置 orderless, 评分函数设置成我上面写的函数,目前只支持 flx-rsflx

(setopt fussy-score-fn 'fussy-orderless-score-with-flx-rs
        fussy-filter-fn 'fussy-filter-orderless-flex)

附录
#


  1. 这个功能默认覆盖来 orderless= 前缀的功能,也就是 orderless-literal, 如果想要共存可以修改为别的字符,而且根据词库大小,可能会出现卡顿。 ↩︎

  2. 计算比较简陋,应该是有更好的算法的。 ↩︎