前面我们以一个简单的 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)))从这段代码的注释上看,它
它主要做了以下事情:
- 定义一个名为
my/focus-mode也就是与mode同名的 buffer-local 的状态变量; - 定义了一个和 mode 同名为
my/focus-mode交互式切换函数; - 定义并配置 hook 变量;
- 把这个 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部分为 nilAFTER和TOGGLE-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)