如何创建一个Emacs Minor Mode
Emacs缓冲区总是会有一个major模式和零个或多个minor模式。major模式实现起来往往非费劲,特别是当涉及到自动缩进时。 相比之下,minor模式通常很简单,可能只是覆盖一小个kemap来实现附加功能。创建一个新的minor模式非常简单,只需理解Emacs的约定即可。
模式名应该以 -mode
结尾,用于切换模式的命令应该与模式名相名。模式的keymap应该被称为 mode=-map=, 模式切换的钩子应该被称为 mode=-hook=.在为minor模式选择名称时,请记住所有这些约定。
在手动构建minor模式时,还需要考虑许多其他繁琐的问题。好消息是,其中大部分都无需让人担心!
Lisp通过宏来减少样板代码,因此有一个宏能用于构建minor模式: define-minor-mode
. 下面是创建一个新的minor模式 foo-mode
所需的全部内容。
(define-minor-mode foo-mode "Get your foos in the right places.")
这将创建一个用于切换minor模式的命令 foo-mode
和一个名为 foo-mode-hook
的钩子。
关于这个钩子有一个奇怪的值得注意的地方:它没有立即声明为一个变量。我猜测这是某种古老的优化措施,但是现在这种设计很糟糕了。
hook函数 add-hook
将在需要时惰性地创建这个变量,而函数 run-hook
将忽略还不存在的hook变量,因此在这种设计下也不会有问题。
总的来说,尽管它的初始状态很奇怪,但新的minor模式将在添加函数后立即使用这个钩子。
minor模式选项
It's just a toggle and a hook that's run when the toggle is used. To add more to the mode, define-minor-mode
accepts a number of keywords. Here are the important ones.
这个模式还不能做任何事情。它没有自己的keymap,甚至不在modeline中显示。
它只有一个切换命令以及一个切换命令运行的钩子。
define-minor-mode
接受一些关键字,能向模式添加更多内容。下面这些是重要的关键字:
:lighter
: 名称,字符串形式,在modeline中显示:keymap
: 模式的 keymap:global
: 指定 minor mode 是否为全局模式
:lighter
选项有一个值得关注的地方: 它会直接拼接到 modeline 后,没有任何分隔符。这意味着它需要以空格作为前缀。我认为该设计是错误的,但我们可能永远都无法摆脱它。否则这个字符串只能很短:modeline上通常没有太多空间。
(define-minor-mode foo-mode "Get your foos in the right places." :lighter " foo")
使用 (make-keymap)
或 (make-sparse-keymap)
创建新的空 keymap。当 keymap 中包含少量键绑定时(大多数minor mode都是这样的),后者的效率更高。
这些不同函数的存在可能是另一个过时的、不成熟的优化。为了避免混淆,我建议使用你常用的那个函数就行。
keymap 可以直接提供给 :keymap
参数,并自动绑定到 foo-mode-map
上.
我也可以在这里放置一个空的keymap,并在 define-minor-mode
声明之外单独定义快捷键,但是我喜欢在一个表达式中创建整个 keymap 的想法。
(defun insert-foo () (interactive) (insert "foo")) (define-minor-mode foo-mode "Get your foos in the right places." :lighter " foo" :keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c f") 'insert-foo) map))
:global
选项意味着minor模式不仅仅作用于当前buffer,而是作用于所有buffer。据我所知,我唯一使用过的global minor模式是 YASnippet.
Minor Mode 主要内容
define-minor-mode
的剩下部分是一段任意Lisp代码,跟 defun
类似。
每次模式被关闭或打开时,它都会运行,所以它就像一个内置的钩子函数。
你可以使用它来进行任何特殊的设置或清理,例如挂钩或取消Emacs的挂钩。
这里通常要做的一件事是指定缓冲区局部变量。
在Emacs解释器运行表达式时,总有一个充当上下文的当前缓冲区。 许多对缓冲区进行操作的函数实际上并不接受缓冲区作为参数。相反,它们对当前缓冲区进行操作。 此外,有些变量是尽在缓冲区本地有效的:它们动态地绑定在当前缓冲区上。 这对维护仅与特定缓冲区相关的状态非常有用。
旁注: with-current-buffer
宏用于为一段代码指定另一个缓冲区作为当前缓冲区。
它可以用来访问其他缓冲区的局部变量。
类似地, with-temp-buffer
则创建一个全新的缓冲区,将其用作主体代码的当前缓冲区,然后销毁该缓冲区。
例如,假设我想记录 foo-mode
向当前缓冲区插入“foo”的次数。
(defvar foo-count 0 "Number of foos inserted into the current buffer.") (defun insert-foo () (interactive) (setq foo-count (1+ foo-count)) (insert "foo")) (define-minor-mode foo-mode "Get your foos in the right places." :lighter " foo" :keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c f") 'insert-foo) map) (make-local-variable 'foo-count))
内置函数 make-local-variable
在当前缓冲区中创建全局变量在新缓冲区中的局部副本。
这里,缓冲区局部的 foo-count
将使用全局变量的值0进行初始化,但是所有的重新赋值只在当前缓冲区中可见。
但是,在本例中,最好在全局变量上使用 make-variable-buffer-local
,并跳过 make-local-variable
语句。
主要原因是,我不希望 insert-foo
在没有启用minor模式的缓冲区中调用时修改了全局变量。
(make-variable-buffer-local (defvar foo-count 0 "Number of foos inserted into the current buffer."))
这种方法有一个很大的优点就是任意地方查看该变量的文档说明,都能看出该变量的作用域局限在缓冲区。此消息会出现在变量的文档中。
Automatically becomes buffer-local when set in any fashion.
你使用哪种方法取决于你的个人喜好。Emacs文档鼓励前者,但我认为后者在许多情况下更好。
自动启用Minor模式
某些minor模式不与任何特定的major模式相关,用户可以随意切换。 某些minor模式则只有在与特定的major模式一起使用时才有意义,通常与这个major模式一起自动启用。 自动启动是通过挂起major模式的钩子来完成的。只要模式遵循前面提到的Emacs的约定,就应该很容易找到这个钩子。
(add-hook 'text-mode-hook 'foo-mode)
这里,=foo-mode= 将在所有 text-mode
buffer中自动激活。
完整代码
下面是minor模式的最终代码,保存为 foo-mode.el
.它有一个快捷键,用户可以很容易地在 foo-mode-map
中定义更多的快捷键。当用户编辑纯文本文件时,它也会自动激活。
(make-variable-buffer-local (defvar foo-count 0 "Number of foos inserted into the current buffer.")) (defun insert-foo () (interactive) (setq foo-count (1+ foo-count)) (insert "foo")) ;;;###autoload (define-minor-mode foo-mode "Get your foos in the right places." :lighter " foo" :keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c f") 'insert-foo) map)) ;;;###autoload (add-hook 'text-mode-hook 'foo-mode) (provide 'foo-mode)
我添加了一些 autoload 声明和一个 provide
以防该模式被作为包分发或使用。为这个minor模式生成了 autoload 脚本会定义一个名为 foo-mode
的临时函数,这个函数的惟一目的就是加载真正的 foo-mode.el
,然后再次调用 foo-mode
, foo-mode
的新定义会覆盖临时定义。
autoload 脚本还将这个临时的 foo-mode
函数添加到 text-mode-hook
中。如果创建了 text-mode
buffer,钩子将调用 foo-mode
,从而加载 foo-mode.el
,重新定义 foo-mode
为实际定义,然后激活 foo-mode
.
autoload 的目的是将加载代码的时间延迟到需要的时候。你可能会注意到,在启动Emacs后第一次激活模式时有一个短暂的延迟.这就是尽管Emacs在启动时装载了数百万行Elisp,但启动时间依然合理的原因。