对于 Emacs 用户来说,优化自己的配置是件乐趣无穷的事情,而且也是成为 Emacs 高手的必经之路。一般来说,新手的配置都是东拼西凑出来的,这是最快最有效的学习途径。随着对 Emacs 使用的加深,配置逐渐复杂,如果不对之前杂乱无章的配置进行重构,很难想象可以继续愉快地使用 Emacs。
这篇文章就来介绍一下我个人优化配置的心得,主要内容:包的加载原理与管理实践,希望对读者优化自己的配置有些帮助。
Package.el 问题
毫不夸张地说,高度的扩展性是 Emacs 延续几十年生生不息的主要原因, (length package-alist) 可以统计通过 package.el 安装的包数量,我是 137 个。
尽管 package.el 提供了一种便捷的方式来安装包,但它并不提供版本管理的功能,这是任何一个包管理器最基础的功能,我曾经多次因为包升级导致功能失效,这是十分让人沮丧的事情,参考这里。
社区有一些解决方案,比如 straight、borg ,但为了避免引入新问题,减轻学习负担,我目前没有采用这些方案,而是用 git 自带的 submodule 来管理一些重度使用的包(比如 lsp-mode/magit),闲暇时再专门去做升级工作,升级出问题直接回退到之前的 commit 即可,再也不用担心被工具打断的烦恼。
包加载原理
对于 package.el 管理的包,用户是无须了解 Emacs 加载包的方式就可以使用,但是如何要自己完全管理,就需要了解这些细节了。首先明确下包的定义:
Emacs 提供了两类高阶接口来进行包的自动加载:Autoload 与 Feature。
Autoload
Autoload 函数可以声明函数或宏,在真正使用时再去加载其对应的文件。
1(autoload filename docstring interactive type)一般不直接使用 autoload 函数,而是 autoload 魔法注释,然后用一些函数来解析魔法注释自动生成 autoload 函数,比如在 my-mode 文件夹内有一文件 hello-world.el ,内容为:
1;;;###autoload
2(defun my-hello ()
3(interactive)
4(message "hello world"))使用下面的命令生成 autoloads 文件
1(package-generate-autoloads "hello-world" "~/my-mode")在同一目录生成 hello-world-autoloads.el 文件,内容为:
1;;; hello-world-autoloads.el --- automatically extracted autoloads
2;;
3;;; Code:
4
5(add-to-list 'load-path (directory-file-name
6 (or (file-name-directory #$) (car load-path))))
7
8
9;;;### (autoloads nil "hello-world" "hello-world.el" (0 0 0 0))
10;;; Generated autoloads from hello-world.el
11
12(autoload 'my-hello "hello-world" nil t nil)
13
14;;;***
15
16;; Local Variables:
17;; version-control: never
18;; no-byte-compile: t
19;; no-update-autoloads: t
20;; coding: utf-8
21;; End:
22;;; hello-world-autoloads.el ends here这意味着只在第一次 M-x my-hello 时,才回去加载 hello-world.el 文件。
这里需要注意,为了让 Emacs 识别到 my-hello 函数的声明,需要去加载 hello-world-autoloads.el 文件,对于通过 package.el 管理的包,package.el 在下载该包时,会进行下面的操作:
- 解析依赖,递归下载
- 把包目录追加到 load-path 中
- 自动生成 autoloads 文件,并且加载它
这样用户就能够直接使用该包提供的函数了。如果使用 submodule 管理,上述操作则需要自己实现,后文会介绍。
Feature
Feature 是 Emacs 提供的另一种自动加载 ELisp 文件的机制,使用示例:
1(defun my-hello ()
2 (interactive)
3 (message "hello world"))
4
5;; feature 名与文件名相同
6(provide 'hello-world)上述代码即生成了一个 feature,名为 hello-world,由于与文件同名,只需在使用 my-hello 前 (require 'hello-world) 即可,这样就会去自动加载 hello-world.el。
feature 的一个优势就是可以防止重复加载,所有被加载的 feature 会被记录在 features 变量中,只有在第一次 require 时才会去加载文件,后续的 require 则会直接返回。如果想要重复加载,需要先调用 unload-feature 进行卸载。
Load
1(load filename &optional missing-ok nomessage nosuffix must-suffix)上面的 autoload 与 feature 底层都是通过调用 load 函数去加载文件,但 load 为相对低级的 API,功能虽然比较丰富,但用起来不如上述两个高级 API 方便,因此一般 Emacs 用户不会直接使用它。
Submodule 管理包
在上面介绍 autoload 时,介绍了 package.el 下载一个包时的大致步骤,这里重新温故下:
- 解析依赖,递归下载
- 把包目录追加到 load-path 中
- 自动生成 autoloads 文件,并且加载它
使用 submodule 的话,只能下载包本身,上述三步都需要自己做,我目前使用 use-package 来下载、配置包,下面通过一个示例来介绍其用法:
1(use-package lsp-mode
2 ;; 配置 load-path,lsp-mode 通过 submodule 下载到 ~/.emacs.d/vendor/lsp-mode 目录内
3 :load-path ("~/.emacs.d/vendor/lsp-mode" "~/.emacs.d/vendor/lsp-mode/clients")
4 :init (setq lsp-keymap-prefix "C-c l")
5 ;; 配置 mode 的 hook
6 :hook ((go-mode . lsp-deferred))
7 ;; 生成 autoloads
8 :commands (lsp lsp-deferred)
9 ;; 配置 custom 变量
10 :custom ((lsp-log-io nil))
11 :config
12 (require 'lsp-modeline)
13 (push "[/\\\\]vendor$" lsp-file-watch-ignored-directories)
14 ;; 配置 mode-map 快捷键
15 :bind (:map lsp-mode-map
16 ("M-." . lsp-find-definition)
17 ("M-n" . lsp-find-references)))可以看到,use-package 宏的使用非常简明扼要,而且把包的各种配置都统一起来了,强烈推荐。使用 macroexpand-1 展开 use-package,会发现和我们手动配置的代码相差无几:
1(progn
2 (eval-and-compile
3 (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode"))
4 (eval-and-compile
5 (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode/clients"))
6
7 (let
8 ((custom--inhibit-theme-enable nil))
9 (unless
10 (memq 'use-package custom-known-themes)
11 (deftheme use-package)
12 (enable-theme 'use-package)
13 (setq custom-enabled-themes
14 (remq 'use-package custom-enabled-themes)))
15 (custom-theme-set-variables 'use-package
16 '(lsp-log-io nil nil nil "Customized with use-package lsp-mode")))
17 (unless
18 (fboundp 'lsp-deferred)
19 (autoload #'lsp-deferred "lsp-mode" nil t))
20 (unless
21 (fboundp 'lsp-find-definition)
22 (autoload #'lsp-find-definition "lsp-mode" nil t))
23 (unless
24 (fboundp 'lsp-find-references)
25 (autoload #'lsp-find-references "lsp-mode" nil t))
26 (unless
27 (fboundp 'lsp)
28 (autoload #'lsp "lsp-mode" nil t))
29 (condition-case-unless-debug err
30 (setq lsp-keymap-prefix "C-c l")
31 (error
32 (funcall use-package--warning139 :init err)))
33 (eval-after-load 'lsp-mode
34 '(progn
35 (require 'lsp-modeline)
36 (push "[/\\\\]vendor$" lsp-file-watch-ignored-directories)
37 t)
38 (add-hook 'go-mode-hook #'lsp-deferred)
39 (bind-keys :package lsp-mode :map lsp-mode-map
40 ("M-." . lsp-find-definition)
41 ("M-n" . lsp-find-references))
42 ))use-package 解决了繁琐的配置问题,但并不能解决包依赖的问题,只能一个个下载(具体依赖见包的 Package-Requires 声明):
1;; lsp-mode deps
2(use-package spinner
3 :defer t)
4(use-package lv
5 :defer t)
6;; ...use-package 在 load-path 中找不到这些依赖时,会自动利用 package.el 去下载。我这里的做法是折中的,对于一些轻量的包,没必要用 submodule 管理。读者可能会觉得这种手动管理依赖的方式会比较繁琐,但是实际上不同包的依赖很有可能是相同的,比如:dash.el、s.el、f.el 等这些基础包,所以实际需要手动管理的依赖不是很多。
use-package bootstrap
1(package-initialize)
2(when (not package-archive-contents)
3 (package-refresh-contents))
4
5(dolist (p '(use-package))
6 (when (not (package-installed-p p))
7 (package-install p)))
8
9(setq use-package-always-ensure t
10 use-package-verbose t)
11
12;; 后面就可以直接使用 use-package 来安装、配置包了常用 Git 命令
1# 修改 .gitmodules 后
2git submodule sync
3
4# 更新到最新 commit
5git submodule update --init --recursive --remote
6
7# https://stackoverflow.com/a/18854453/2163429
8# 更新到 .gitmodules 中指定的 commit
9git submodule update --init对于 submodule 的增加与删除,直接在 magit 中操作就好了, magit-status-mode 下按 o 即可。