《elisp 每日一练》—— Minor Mode 变量生成机制与状态控制

《elisp 每日一练》—— Minor Mode 变量生成机制与状态控制

Masimaro
2026-09-10 / 0 评论 / 0 阅读 / 正在检测是否收录...

前面我们以一个简单的 dired-mode 实战案例结束了关于文件和目录专题的学习。现在开始进入Emacs中的Minor Mode 和 Major Mode的学习。

define-minor-mode

在前面我们已经使用了 define-minor-mode 定义了一个 windows-notepad 的mode,它主要绑定Windows常用的快捷键,例如:C-c 复制、C-v 粘贴。

我们定义一个最简单的minor mode

(define-minor-mode my/focus-mode
  "这是一个专注模式。")

对于这种使用宏的代码,我们可以使用将光标放在代码块的最后使用 pp-macroexpand-last-sexp 来得到展开之后的内容。上述代码展开之后得到如下内容

(progn
  (progn
    :autoload-end
    (defvar-local my/focus-mode nil
      "Non-nil if My/Focus mode is enabled.\nUse the command `my/focus-mode' to change this variable."))
  (defun my/focus-mode (&optional arg)
    "这是一个专注模式。\n\nThis is a minor mode.  If called interactively, toggle the `My/Focus\nmode' mode.  If the prefix argument is positive, enable the mode, and if\nit is zero or negative, disable the mode.\n\nIf called from Lisp, toggle the mode if ARG is `toggle'.  Enable the\nmode if ARG is nil, omitted, or is a positive number.  Disable the mode\nif ARG is a negative number.\n\nTo check whether the minor mode is enabled in the current buffer,\nevaluate the variable `my/focus-mode'.\n\nThe mode's hook is called both when the mode is enabled and when it is\ndisabled."
    (interactive
     (list
      (if current-prefix-arg (prefix-numeric-value current-prefix-arg)
        'toggle)))
    (let ((last-message (current-message)))
      (setq my/focus-mode
            (cond ((eq arg 'toggle) (not my/focus-mode))
                  ((and (numberp arg) (< arg 1)) nil) (t t)))
      (when (boundp 'local-minor-modes)
        (setq local-minor-modes
              (delq 'my/focus-mode local-minor-modes))
        (when my/focus-mode (push 'my/focus-mode local-minor-modes)))
      (run-hooks 'my/focus-mode-hook
                 (if my/focus-mode 'my/focus-mode-on-hook
                   'my/focus-mode-off-hook))
      (if (called-interactively-p 'any)
          (progn
            nil
            (unless
                (and (current-message)
                     (not (equal last-message (current-message))))
              (let ((local " in current buffer"))
                (message "%s %sabled%s" "My/Focus mode"
                         (if my/focus-mode "en" "dis") local))))))
    (force-mode-line-update) my/focus-mode)
  :autoload-end (defvar my/focus-mode-hook nil)
  (unless (get 'my/focus-mode-hook 'variable-documentation)
    (put 'my/focus-mode-hook 'variable-documentation
         "Hook run after entering or leaving `my/focus-mode'.\nNo problems result if this variable is not bound.\n`add-hook' automatically binds it.  (This is true for all hook variables.)"))
  (put 'my/focus-mode-hook 'custom-type 'hook)
  (put 'my/focus-mode-hook 'standard-value (list nil)) nil
  (with-no-warnings
    (add-minor-mode 'my/focus-mode 'nil
                    (if (boundp 'my/focus-mode-map) my/focus-mode-map)
                    nil nil)))

从这段代码的注释上看,它

它主要做了以下事情:

  1. 定义一个名为 my/focus-mode 也就是与mode同名的 buffer-local 的状态变量;
  2. 定义了一个和 mode 同名为 my/focus-mode 交互式切换函数;
  3. 定义并配置 hook 变量;
  4. 把这个 minor mode 注册到 Emacs 的 minor mode 系统里。

我们依次来阅读一下这段代码,看看各个部分是如何工作的。

首先我们看到 my/focus-mode 函数中的注释,这段注释详细的介绍了这段代码的作用,我们翻译一下得到如下内容

这是一个次要模式。如果以交互方式调用该模式,则会切换“My/Focus模式”。如果前缀参数为正数,则启用该模式;如果为负数或零,则禁用该模式。
如果通过Lisp调用该模式,并且ARG的值为“toggle”,则切换该模式。如果ARG为nil、被省略,或者是一个正数,则启用该模式。如果ARG为负数,则禁用该模式。
要检查当前缓冲区中是否启用了该次要模式,可以评估变量`my/focus-mode`的值。
在该模式被启用和禁用时,都会调用相应的钩子函数。

从这里可以看出,实际上切换该mode的状态实际上是在修改 my/focus-mode 变量的值。

my/foucs-mode 函数

下面我们来看一下该函数中的重点操作

1. 设置变量值切换mode 状态

我们先来看一下最前面的 interactive 它的内容如下:

(interactive
     (list
      (if current-prefix-arg (prefix-numeric-value current-prefix-arg)
        'toggle)))

这段代码的意思是如何用户调用时没有参数,那么设置它的值为 'toggle 表示切换。如果有使用 C-u 输入前置参数,那么它会尝试将输入的参数转为数字。

后面紧跟着的就是对 my/focus-mode 的赋值,它的代码如下:

(setq my/focus-mode
    (cond ((eq arg 'toggle) (not my/focus-mode))
        ((and (numberp arg) (< arg 1)) nil) (t t)))

它的赋值规则很简单,如果是 'toggle ,那么对原值取反以便进行切换,如果是数字,负数和0的时候关闭mode,也就是将 my/focus-mode 设置为ni,正数则设置为 t

2. 维护 local-minor-modes

接着我们往后看

(when (boundp 'local-minor-modes)
  (setq local-minor-modes
        (delq 'my/focus-mode local-minor-modes))
  (when my/focus-mode
    (push 'my/focus-mode local-minor-modes)))

local-minor-modes 是一个变量,它记录当前buffer中启用了哪些 minor mode。上面的代码先判断该变量是否绑定了值,如果绑定了,则先删除当前mode,后面根据 my/focus-mode 的真假来决定是否将当前mode添加到变量中

3. 运行hook

hook 的概念我们还没有开始接触,但是如果读者对其他编程语言熟悉,相比对hook应该也不陌生。这里你可以简单的类比事件的回调。它提供了一个外部干预事件的契机。

(run-hooks 'my/focus-mode-hook
           (if my/focus-mode
               'my/focus-mode-on-hook
             'my/focus-mode-off-hook))

从这里我们能看出来,不管启用还是关闭,都会调用 my/foucus-mode-hook,后续如果启用则会调用 my/focus-mode-on-hook。关闭会调用 my/focus-mode-off-hook

4. 刷新mode line 并返回状态

函数最后一句代码如下:

    (force-mode-line-update) 
    my/focus-mode)

force-mode-line-update 用来刷新 modeline 的状态,最后对 my/focus-mode 求值并返回它的值。

定义并配置 hook 变量

函数结束之后,它又定义了一个 my/focus-mode-hook 这个变量我们刚刚看到,它在函数中被使用。

(defvar my/focus-mode-hook nil)

我们看到该变量没有采用 defvar-local 定义成 buffer-local 类型的变量,而是使用 defvar 定义成了全局变量。Emacs 中的hook变量通常都是全局变量,这个我们在讨论hook的时候再来解释

(unless (get 'my/focus-mode-hook 'variable-documentation)
  (put 'my/focus-mode-hook 'variable-documentation
       "Hook run after entering or leaving `my/focus-mode'..."))

这段代码虽然我们不知道 variable-documentation 是个什么东西,但是从名字上应该能猜出来,它是变量文档。我们通过 C-h v 查看hook变量时显示的内容。它的作用是如果没有指定 hook变量的文档,它帮我们指定一个默认的变量文档。

接着我们看到两个 put

(put 'my/focus-mode-hook 'custom-type 'hook)
(put 'my/focus-mode-hook 'standard-value (list nil))

它向 custom 系统注册了 my/focus-mode-hook 的信息,我们可以使用 customize 命令打开 custom 的界面,查到该变量的信息。它们设置变量类型为 hook、标准值为空列表。

最后以下面的代码作为结尾:

(with-no-warnings
  (add-minor-mode 'my/focus-mode 'nil
                  (if (boundp 'my/focus-mode-map)
                      my/focus-mode-map)
                  nil nil))

我们重点关注 add-minor-mode 部分的代码,它是一个底层C写的函数,负责将该mode注册到Emacs 管理的 minor mode 系统中。它的函数签名为:

(add-minor-mode TOGGLE NAME &optional KEYMAP AFTER TOGGLE-FUN)

根据函数签名,我们可以看到,注册时填写了如下信息:

  • 切换状态的变量为 my/focus-mode
  • NAME 为 nil,也就是不在mode line 上显示mode 的名称
  • KEYMAP 部分的参数表示该mode下的 keymap 。叠加一个if语句,表明如果有名为 my/foucus-mode-map 的keymap则采用它作为keymap,否则keymap部分为 nil
  • AFTERTOGGLE-FUN 两个参数为 nil。

总结

到此我们描述完了 define-minor-mode 宏在底层所干的事情:

  • 定义切换变量,与mode名称相同
  • 定义切换函数,与mode名称相同
  • 切换时根据传入参数来切换mode状态:为负数或者0表示关闭,1表示开启,'toggle 表示根据当前状态切换
  • 切换状态时会无条件执行 my/foucs-mode-hook ,在开启和关闭时则会再额外单独执行 my/foucs-mode-on-hook 或者 my/foucs-mode-off-hook
  • 中间它还会根据mode状态维护 local-minor-mode 变量、注册minor mode 到Emacs,更新显示一些内容

本节到此就结束了,本节因为主要是理论性的内容,不涉及实操,所以就不进行练习了,如果说非要按照惯例进行练习的话,读者可以自行查看源代码并跟着本文的内容梳理一下思路。或者也可以尝试根据文档使用诸如 :keymap:init-value 之类的关键字看看它们在背后都做了什么

0

评论 (0)

取消