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

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

define-minor-mode

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

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

emacs-lisp 复制代码
(define-minor-mode my/focus-mode
  "这是一个专注模式。")

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

emacs-lisp 复制代码
(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 函数中的注释,这段注释详细的介绍了这段代码的作用,我们翻译一下得到如下内容

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

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

my/foucs-mode 函数

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

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

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

emacs-lisp 复制代码
(interactive
	 (list
	  (if current-prefix-arg (prefix-numeric-value current-prefix-arg)
		'toggle)))

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

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

emacs-lisp 复制代码
(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

接着我们往后看

emacs-lisp 复制代码
(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应该也不陌生。这里你可以简单的类比事件的回调。它提供了一个外部干预事件的契机。

emacs-lisp 复制代码
(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 并返回状态

函数最后一句代码如下:

emacs-lisp 复制代码
    (force-mode-line-update) 
    my/focus-mode)

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

定义并配置 hook 变量

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

emacs-lisp 复制代码
(defvar my/focus-mode-hook nil)

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

emacs-lisp 复制代码
(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

emacs-lisp 复制代码
(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、标准值为空列表。

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

emacs-lisp 复制代码
(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 系统中。它的函数签名为:

emacs-lisp 复制代码
(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
  • AFTER 和 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 之类的关键字看看它们在背后都做了什么

相关推荐
智购科技自动售货机工厂2 天前
数字人民币硬钱包支付失败,排查发现是NFC读卡器功率不足~YH
python·面试·架构·eclipse·emacs
Masimaro3 天前
《elisp 每日一练》—— define-derived-mode 管理 syntax-table
emacs
Masimaro11 天前
《elisp 每日一练》—— Major Mode 管理 abbrev-table
emacs
AI情绪识别开源12 天前
检信AI学业生涯智选系统技术方案概述
emacs
Masimaro16 天前
《elisp 每日一练》—— Minor Mode 回顾与综合练习
emacs
aluluka16 天前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs·lisp·elisp
aluluka18 天前
《elisp 每日一练》——minor mode 快捷键管理
emacs·lisp·elisp
Masimaro18 天前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs
Masimaro20 天前
《elisp 每日一练》——minor mode 开关变量的管理
emacs