EMACS-DOCUMENT

=============>随便,谢谢

如何创建一个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,但启动时间依然合理的原因。