《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
  • 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 之类的关键字看看它们在背后都做了什么

相关推荐
Masimaro1 天前
《elisp 每日一练》——文件与目录的回顾与综合练习
emacs
Masimaro8 天前
《elisp 每日一练》——minibuffer输入读取和set-transient-map 临时热键状态机
emacs
Masimaro11 天前
《elisp 每日一练》——Window 树与布局状态恢复
emacs
典典分享指南13 天前
Windows 自动化踩坑实录
python·intellij-idea·pyqt·emacs·visual studio
辰辉创聚14 天前
重组细胞因子全解析:定义、分类与应用指南
人工智能·eclipse·emacs·白细胞介素·重组蛋白·重组细胞因子
Masimaro16 天前
《elisp 每日一练》—— 前缀按键
emacs
带娃的IT创业者19 天前
Emacs 31:一场静默而深刻的范式演进
编辑器·文本编辑器·开源软件·emacs·lisp·软件架构
Masimaro21 天前
《elisp 每日一练》——syntax-table
emacs
Masimaro1 个月前
《elisp 每日一练》—— overlay 机制
emacs