EMACS-DOCUMENT

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

定义minor mode的几点说明

今天的帖子技术含量很高,但我想分享一个几天前的有趣故事。

我使用经典架构定义了一个minor mode

(defun cool-start ()
  "Start the cool-mode.")

(defun cool-stop ()
  "Stop the cool-mode.")

(define-minor-mode my-cool-minor-mode
  "Toggle a mode for doing cool stuff."
  :init-value nil
  :lighter " 8-)"
  (if my-cool-minor-mode
      (cool-start)
    (cool-stop)))

我很好奇是否可以使用前缀参数来决定在启动模式时做什么。这似乎有点冒险,因为前缀参数已经用于决定打开或关闭模式了。

实际上, my-cool-minor-mode 命令的参数的文档点误导人。以下是Emacs手册中的一段摘录:

  • 如果你直接调用mode命令而不带任何前缀参数(通过“M-x”,或通过绑定到一个键并输入该键[…]),就会“切换” minor-mode。minor-mode若关闭则打开,若打开则关闭。
  • 如果你使用前缀参数调用mode命令,则若参数为0或负数,minor-mode将无条件关闭;否则,将被无条件地打开。
  • 如果通过Lisp调用mode命令,则若参数被省略或为“nil”,则minor-mode将无条件打开。这使得通过major mode的mode hook来打开minor-mode变得很容易[...]。非'nil'参数的处理方式类似于前面描述的交互式前缀参数。

以下是Elisp手册中的一段话:

toggle命令接受一个可选的(前缀)参数。如果交互方式调用则没有参数的情况下,它切换打开或关闭模式,正的前缀参数启用该模式,任何其他前缀参数将禁用该模式。 在Lisp中调用的话,‘toggle’参数切换模式,而省略或‘nil’参数启用该模式。例如,这可以使通过major mode的mode hook来打开minor mode变得很容易. 如果DOC为“nil”,则宏提供一个默认的文档字符串来解释上述内容。

正如你所见,从Elisp手册中并不完全清楚是否可以通过代码提供数值参数(显然,您不能交互地提供='toggle=参数)。

原来, define-minor-mode 宏以一种非常复杂的方式定义了负责打开和关闭模式的函数。

首先,该宏包含了下面片段:

(interactive (list (or current-prefix-arg 'toggle)))

这意味着没有参数(在交互调用时)与 'toggle 参数完全等价。

另一个技巧是:

(,@setter
 (if (eq arg 'toggle)
     (not ,getter)
   ;; A nil argument also means ON now.
   (> (prefix-numeric-value arg) 0)))

define-minor-mode 在前面将 setter 设置成了 (setq <mode-name>), 将 getter 设置成了 <mode-name>. 这里有一个非常聪明的技巧: getter 变量其实就是mode,而 setter 是将 getter 变量设置为某个值的Elisp form的开头部分。 (如果您认为这太抽象,让我告诉您,我的描述还算是简化过得:例如,对于全局模式来说, settergetter 就跟这里说的有所不同。)

正如您从上面的技巧中看到的,我的原始问题有一个简单的答案:是的,我们可以在Elisp代码中提供数值参数,实际上,提供一个负参数是通过编程手段关闭模式的唯一方法。

了解了这一点,我们现在可以着手解决最初的问题了。既然任何正的前缀参数都意味着“打开模式”,那么我们可以使用它的实际值来做各种事情吗?

答案当然是肯定的。我们可以有两个选择。首先,我们的模式主体可以检查 current-prefix-arg. 此外我们还可以使用 arg.(define-minor-mode 宏就是通过该参数调用新定义的切换函数的)。 后者显然没有那么明显(特别是它依赖于的实现细节可能在另一个Emacs版本中更改),所以让我们忘记它吧。(实际上,与其使用 arg 符号,不如使用一些类似于Common Lisp gensym 的东西。Elisp没有该函数. cl 包有 cl-gensym, 但是内置的特性并不依赖于 cl 包,这是可以理解的。

但是故事还没结束呢。 还有一些关键字参数在 define-minor-mode 的docstring和手册中都尚未提及. 其中一个就是 :extra-args. (有趣的旁注: 该 :extra-args 关键字在整个Emacs源中只提到了三次. 根据 git blame 的结果,其中一次是定义,最后一次由 Stefan Monnier 在2012-06-10使用。 第二次在 use-hard-newlines minor mode中,最后一次由 Stefan Monnier 在 2001-10-30 使用。 第三次是在 global-font-lock-mode 的注释中提及,该注释写的诗 What was this :extra-args thingy for? --Stef, 最后一次在2009-09-13使用,作者你是猜猜是谁?)

All these arguments (including the first one) are optional, and you can't supply the :extra-args on an interactive call. You can, however, supply them from Elisp code. Here is an example. 你可以这样使用它。在 :extra-args 关键字之后的值应该是一个(unqouted的)列表,这个列表会被添加到打开本模式的函数的第一个参数之后(例如,在以交互方式调用mode命令的前缀参数)。 所有这些参数(包括第一个参数)都是可选的,您不能在交互式调用中提供 :extra-args. 但是,你可以通过Elisp代码提供这些参数。例如:

(define-minor-mode my-cool-minor-mode
  "Toggle a mode for doing cool stuff."
  :init-value nil
  :lighter " 8-)"
  :extra-args (cool-arg-1 cool-arg-2)
  (if my-cool-minor-mode
      (progn
        (cool-start)
        (message "cool-arg-1: %s, cool-arg-2: %s" cool-arg-1 cool-arg-2))
    (cool-stop)))

试着执行: M-:(my-cool-min -mode 1 "this") 看看 cool-arg-1 是否变成了 "this" , cool-arg-2 变成 nil。

最后一个关于 define-minor-mode 的趣闻是 My-Cool minor mode 在当前buffer启用时 的默认消息. 我注意到 message 函数放到mode的启动代码中时,该默认消息不会现实。 为了找到原因,一开始我在Emacs源码中搜索单词 “enabled” (2774 处) 和 “disabled” (1324 处). 没有结果。 然后我 回想起了 debug-on-message 变量, 结果发现我的搜索完全没有. 原因是(简化了一点):

(message
 "Some-mode %sabled"
 (if mode-variable "en" "dis"))

好吧,这让我哭笑不得(特别是我必须要处理一点软件国际化的事情),但我承认它在某种程度上是个不错的做法。

更有趣的是,如果负责初始化(或关闭)模式的代码提供了自己的 message,那么“enable/disable”消息实际上是会关闭的。这是在 current-message 函数的帮助下完成的,该函数返回当前在echo区域中显示的内容。

总之,我只是简单地了解了下表层。如果你深入文件 easy-mmode.el (所有代码都在该文件中),你会发现相当多的细节(比如 easy-mmode-pretty-mode-name 有数十行代码但只做一件事情那就是将mode符号转换成方便人阅读的形式--使用mode的 lighter 参数来推断大写形式!)。 这是Emacs开发人员非常关注细节的另一个例子,即使在开发过程中存在一些有问题的实践。mb-wink.gif