面向产品经理的Emacs教程:7. Emacs的补全强化

· 2858字 · 6分钟

1 课程回顾 🔗

在上一节课中,我们通过设置,改变了Emacs的一些默认行为如自动备份、选择文本后输入进行替换等,让我们的Emacs更加符合日常使用习惯。我们也通过一些插件,如 org-auto-tangle 等插件实现了自动 tangle 等功能,极大的方便了我们通过 Org 文件对配置进行模块化的管理。

今天,我们将对Emacs自带的补全系统做一个增强,让Emacs在效率上更进一步。

2 Emacs自带的补全系统 🔗

在我们尝试用 C-x C-f 打开某个文件时,可以按 TAB 键触发Emacs自带的补全系统,这样它会把当前目录下可能的文件名列出来,你可以按某个字母后再按 TAB 键进行进一步的补全。如下面的动图:

自带的补全系统当然不够好用,不够直观,也不支持模糊匹配,筛选结果只能按照它提示的字母,按顺序进行筛选。

我们希望达到的效果是,凭着我们记忆中的一些关键词,顺序也不重要,通过这些关键词的组合来筛选匹配出候选的结果。例如,我希望筛选出的是所有包含 emacsorg 的文件名,它应该自动帮我过滤筛选出所有文件名包含 emacsorg 的文件名,从而达到快速选择快速定位的目的。

3 Emacs的第三方补全体系 🔗

Emacs有很多很好用的补全体系,如 ivy 体系,vertico 体系等等,这里我们推荐新手朋友使用的是 vertico ,当然,将来你完全可以按照自己的意愿和喜好去选择喜欢的补全体系,毕竟Emacs是自由的!

vertico 体系包含以下几个插件:

  • vertico
  • consult
  • corfu
  • marginalia
  • orderless

所谓的补全体系包含这几个插件,也只是我个人的看法,并不代表必须这么搭配,只是在社区,这几个插件的组合更加适合而已。

下面我们一个一个来安装配置这些生产力中的生产力!

3.1 vertico 🔗

vertico 插件提供了一个垂直样式的补全系统。

(use-package vertico
  :ensure t
  :hook (after-init . vertico-mode)
  :bind (:map minibuffer-local-map
              ("M-<DEL>" . my/minibuffer-backward-kill)
              :map vertico-map
              ("M-q" . vertico-quick-insert)) ; use C-g to exit
  :config
  (defun my/minibuffer-backward-kill (arg)
    "When minibuffer is completing a file name delete up to parent
folder, otherwise delete a word"
    (interactive "p")
    (if minibuffer-completing-file-name
        ;; Borrowed from https://github.com/raxod502/selectrum/issues/498#issuecomment-803283608
        (if (string-match-p "/." (minibuffer-contents))
            (zap-up-to-char (- arg) ?/)
          (delete-minibuffer-contents))
      (backward-kill-word arg)))

  ;; Do not allow the cursor in the minibuffer prompt
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))
  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

  (setq vertico-cycle t)                ; cycle from last to first
  :custom
  (vertico-count 15)                    ; number of candidates to display, default is 10
  )

安装完 vertico 后,我们可以看到我们打开文件时,不用按 TAB 触发,所有的候选默认展现出来,变成一列,这样比自带的平铺的候选更加直观方便。

3.2 orderless 🔗

orderless 插件提供一种无序的补全新姿势,将一个搜索的范式变成数个以空格分隔的部分,各部分之间没有顺序,你要做的就是根据记忆输入关键词、空格、关键词。

orderless 在我看来,是一个杀手级应用,它改变了我们使用和思考的习惯,我们不再需要关心信息的顺序,我们只需要在脑海中搜索关键信息片段,然后把这些片段组合起来即可,剩下的都交给Emacs。

在这里,还有一个很重要的插件是 pinyinlib ,他能够让 orderless 支持中文的首字母匹配,对效率的提升又大了一截,我们对于中文的文件名的搜索和过滤将更加方便!

;; support Pinyin first character match for orderless, avy etc.
(use-package pinyinlib
  :ensure t)

;; orderless 是一种哲学思想
(use-package orderless
  :ensure t
  :init
  (setq completion-styles '(orderless partial-completion basic))
  (setq orderless-component-separator "[ &]") ; & is for company because space will break completion
  (setq completion-category-defaults nil)
  (setq completion-category-overrides nil)
  :config
  ;; make completion support pinyin, refer to
  ;; https://emacs-china.org/t/vertico/17913/2
  (defun completion--regex-pinyin (str)
    (orderless-regexp (pinyinlib-build-regexp-string str)))
  (add-to-list 'orderless-matching-styles 'completion--regex-pinyin)
  )

安装配置完 orderless ,Emacs开始彰显威力,此时我们想找出当前文件夹下包含 emacsorg 关键词的时候,只需要输入 emacs org 即可:

3.3 marginalia 🔗

marginalia 插件给迷你缓冲区的补全候选条目添加一些提示信息。

(use-package marginalia
  :ensure t
  :hook (after-init . marginalia-mode)
  :custom
  (marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)))

安装了这个插件后,可以看到,我们在选择文件时,迷你缓冲区里加上了文件的权限、大小、修改时间等信息,极大的便利了我们对文件进行选择和判断。

3.4 consult 🔗

consult 插件提供了一系列的查找和补全的命令,非常方便。

(use-package consult
  :ensure t
  :after org
  :bind (([remap goto-line]                     . consult-goto-line)
         ([remap isearch-forward]               . consult-line-symbol-at-point) ; my-consult-ripgrep-or-line
         ([remap switch-to-buffer]              . consult-buffer)
         ([remap switch-to-buffer-other-window] . consult-buffer-other-window)
         ([remap switch-to-buffer-other-frame]  . consult-buffer-other-frame)
         ([remap yank-pop]                      . consult-yank-pop)
         ([remap apropos]                       . consult-apropos)
         ([remap bookmark-jump]                 . consult-bookmark)
         ([remap goto-line]                     . consult-goto-line)
         ([remap imenu]                         . consult-imenu)
         ([remap multi-occur]                   . consult-multi-occur)
         ([remap recentf-open-files]            . consult-recent-file)
         ("C-x j"                               . consult-mark)
         ("C-c g"                               . consult-ripgrep)
         ("C-c f"                               . consult-find)
         ("\e\ef"                               . consult-locate) ; need to enable locate first
         ("C-c n h"                             . my/consult-find-org-headings)
         :map org-mode-map
         ("C-c C-j"                             . consult-org-heading)
         :map minibuffer-local-map
         ("C-r"                                 . consult-history)
         :map isearch-mode-map
         ("C-;"                                 . consult-line)
         :map prog-mode-map
         ("C-c C-j"                             . consult-outline)
         )
  :hook (completion-list-mode . consult-preview-at-point-mode)
  :init
  ;; Optionally configure the register formatting. This improves the register
  ;; preview for `consult-register', `consult-register-load',
  ;; `consult-register-store' and the Emacs built-ins.
  (setq register-preview-delay 0
        register-preview-function #'consult-register-format)

  ;; Optionally tweak the register preview window.
  ;; This adds thin lines, sorting and hides the mode line of the window.
  (advice-add #'register-preview :override #'consult-register-window)

  ;; Use Consult to select xref locations with preview
  (setq xref-show-xrefs-function #'consult-xref
        xref-show-definitions-function #'consult-xref)

  ;; MacOS locate doesn't support `--ignore-case --existing' args.
  (setq consult-locate-args (pcase system-type
                              ('gnu/linux "locate --ignore-case --existing --regex")
                              ('darwin "mdfind -name")))
  :config
  (consult-customize
   consult-theme
   :preview-key '(:debounce 0.2 any)
   consult-ripgrep consult-git-grep consult-grep
   consult-bookmark consult-recent-file consult-xref
   consult--source-recent-file consult--source-project-recent-file consult--source-bookmark
   :preview-key (kbd "M-."))

  ;; Optionally configure the narrowing key.
  ;; Both < and C-+ work reasonably well.
  (setq consult-narrow-key "<") ;; (kbd "C-+")

  (autoload 'projectile-project-root "projectile")
  (setq consult-project-root-function #'projectile-project-root)

  ;; search all org file headings under a directory, see:
  ;; https://emacs-china.org/t/org-files-heading-entry/20830/4
  (defun my/consult-find-org-headings (&optional match)
    "find headngs in all org files."
    (interactive)
    (consult-org-heading match (directory-files org-directory t "^[0-9]\\{8\\}.+\\.org$")))

  ;; Use `consult-ripgrep' instead of `consult-line' in large buffers
  (defun consult-line-symbol-at-point ()
    "Consult line the synbol where the point is"
    (interactive)
    (consult-line (thing-at-point 'symbol)))
  )

安装了 consult 后,我们可以将 C-s 绑定到 consult-line 这个命令,使用 vertico+orderless,很容易能够搜索并跳转到我们想要去的行,在切换不同的候选时,还能预览候选行的内容,非常方便!

之前提到过一个问题,我们要怎么跳转到org文件的某一个标题行呢?当我们安装完 consult 插件后,这个问题可以得到解答了,我们可以使用 consult-org-heading 命令(绑定到了 C-c C-j 按键)来跳转到我们想要去的标题,结合 orderless 我们能快速定位标题行:

3.5 corfu 🔗

corfu 插件可以让我们通过弹窗进行补全。

(use-package corfu
  :ensure t
  :hook (after-init . global-corfu-mode)
  :bind
  (:map corfu-map
        ("SPC" . corfu-insert-separator)    ; configure space for separator insertion
        ("M-q" . corfu-quick-complete)      ; use C-g to exit
        ("TAB" . corfu-next)
        ([tab] . corfu-next)
        ("S-TAB" . corfu-previous)
        ([backtab] . corfu-previous))
  :config
  ;; TAB cycle if there are only few candidates
  (setq completion-cycle-threshold 0)
  (setq tab-always-indent 'complete)

  (defun corfu-enable-always-in-minibuffer ()
    "Enable Corfu in the minibuffer if Vertico/Mct are not active."
    (unless (or (bound-and-true-p mct--active)
                (bound-and-true-p vertico--input))
      ;; (setq-local corfu-auto nil) Enable/disable auto completion
      (corfu-mode 1)))
  (add-hook 'minibuffer-setup-hook #'corfu-enable-always-in-minibuffer 1)

  ;; enable corfu in eshell
  (add-hook 'eshell-mode-hook
            (lambda ()
              (setq-local corfu-auto nil)
              (corfu-mode)))

  ;; For Eshell
  ;; ===========
  ;; avoid press RET twice in Eshell
  (defun corfu-send-shell (&rest _)
    "Send completion candidate when inside comint/eshell."
    (cond
     ((and (derived-mode-p 'eshell-mode) (fboundp 'eshell-send-input))
      (eshell-send-input))
     ((and (derived-mode-p 'comint-mode)  (fboundp 'comint-send-input))
      (comint-send-input))))

  (advice-add #'corfu-insert :after #'corfu-send-shell)

  :custom
  (corfu-cycle t)                ;; Enable cycling for `corfu-next/previous'
  )

安装配置了 corfu 后,我们在写配置时,就可以通过一个弹窗来进行补全,配合 orderless,我们可以很方便的通过输入多个关键词来筛选我们需要的函数了。

3.5.1 cape 🔗

Cape 插件提供了一系列开箱即用的补全后端,跟corfu联合使用。

(use-package cape
  :ensure t
  :init
  ;; Add `completion-at-point-functions', used by `completion-at-point'.
  (add-to-list 'completion-at-point-functions #'cape-file)
  (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-keyword)  ; programming language keyword
  (add-to-list 'completion-at-point-functions #'cape-ispell)
  (add-to-list 'completion-at-point-functions #'cape-dict)
  (add-to-list 'completion-at-point-functions #'cape-symbol)   ; elisp symbol
  (add-to-list 'completion-at-point-functions #'cape-line)

  :config
  (setq cape-dict-file (expand-file-name "etc/hunspell_dict.txt" user-emacs-directory))

  ;; for Eshell:
  ;; ===========
  ;; Silence the pcomplete capf, no errors or messages!
  (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-silent)

  ;; Ensure that pcomplete does not write to the buffer
  ;; and behaves as a pure `completion-at-point-function'.
  (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-purify)
  )

4 yasnippet模板补全 🔗

当我们配置完 vertico, orderless, marginalia, consult, corfu 后,我们会发现Emacs的便利之处。除了 vertico 补全体系完,我们还需要一个模板体系,如当我们想在 org 文件里添加一个代码块时,需要手动输入 #+begin_src emacs-lisp#+end_src 代码块的头行和尾行,然后在里面编写相关的配置,非常麻烦。

yasnippet 插件是恰恰是为了解决这个问题的一个模板补全系统。yasnippet的安装和配置非常简单:

(use-package yasnippet
  :ensure t
  :diminish yas-minor-mode
  :hook ((after-init . yas-reload-all)
         ((prog-mode LaTeX-mode org-mode) . yas-minor-mode))
  :config
  ;; Suppress warning for yasnippet code.
  (require 'warnings)
  (add-to-list 'warning-suppress-types '(yasnippet backquote-change))

  (setq yas-prompt-functions '(yas-x-prompt yas-dropdown-prompt))
  (defun smarter-yas-expand-next-field ()
    "Try to `yas-expand' then `yas-next-field' at current cursor position."
    (interactive)
    (let ((old-point (point))
          (old-tick (buffer-chars-modified-tick)))
      (yas-expand)
      (when (and (eq old-point (point))
                 (eq old-tick (buffer-chars-modified-tick)))
        (ignore-errors (yas-next-field))))))

所有的模板文件,需要放在 ~/.emacs.d/snippets 文件夹里,针对不同的模式,需要放到不同的文件夹。如在 org-mode 里,我们创建了两个模板文件,在 emacs-lisp-mode 里,我们也创建了两个模板文件:

/Users/randolph/.emacs.d/snippets
├── emacs-lisp-mode
│   ├── defun
│   └── usepackage
├── org-mode
│   ├── emacslisp
│   ├── orgsrc
...

我们以其中 org-mode/emacslisp 的模板为例:

# -*- mode: snippet -*-
# name: emacslisp
# key: <el
# --
#+BEGIN_SRC emacs-lisp
$0
#+END_SRC

创建好这个文本文件后,我们只需要在 org 文件里,输入 <el 后,按 TAB 键后,yasnippet会自动帮我们补全成下面这3行,然后光标会自动放在第二行,非常方便:

#+BEGIN_SRC emacs-lisp

#+END_SRC

至于 yansippet 的模板应该怎么写,我想通过上面那个例子,你应该可以照葫芦画瓢写一些简单的模板,如果需要更复杂的功能,可以参考:yasnippet manual

5 结语 🔗

经过今天的课程,我们给Emacs添加上了非常强大的补全体系,进一步提升了我们使用Emacs的效率。

配置文件的快照见:emacs-config-l7.org

你也可以在 这里 查看最新的配置文件。