首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
298 阅读
2
从零开始配置 vim(15)——状态栏配置
175 阅读
3
nvim番外之将配置的插件管理器更新为lazy
163 阅读
4
2018总结与2019规划
154 阅读
5
PDF标准详解(五)——图形状态
128 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
464
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
5
篇与
的结果
2026-09-15
《elisp 每日一练》—— Minor Mode 回顾与综合练习
到此,我们关于 Minor Mode 的介绍差不多就完结了,按照惯例,我们先回顾一下之前的内容,然后展开今天的练习往期回顾前面我们花了一些篇章,介绍了定义 Minor Mode 的宏 define-minor-mode。它的主要流程是:定义Mode 切换的标志变量定义 Mode 切换的函数管理 Mode 对应的全局hook 变量调用 add-minor-mode 往Emacs中注册一个 Minor Mode切换函数主要的功能是:根据标志变量的值更新数据结构:local-minor-modes 注册或者删除对应的项无条件的执行同名的hook,并且在开启和关闭时会执行对应的 on hook 和 off hook更新显示信息后面我们介绍了它是如何通过一些关键字管理对应的内容::init-value 可以控制标志变量初始值为 t 或者 nil 表示注册时就开启或者后续由用户自己开启:global 将标志变量定义为 customize 变量,这样一处buffer启用,全局启用:keymap 管理我们的keymap定义,会将我们在此关键字下定义的 keymap 注册到 add-minor-mode 中:ligher 定义我们在mode line 上的显示,它会将内容注册到 add-minor-mode ,由该函数将内容追加到 minor-mode-alist 中body 代码部分,这部分用来进行相关配置代码的变更或者恢复这些内容看似很多,实际上都是围绕着 define-minor-mode 和 add-minor-mode 进行,读者感兴趣的话可以深入研究一下这个宏和函数的实现,相信能从中挖掘到更多宝藏。练习回顾完了之后,我们进入今天的练习。前面我们发现 :keymap 、:lighter 注册的顺序都与 add-minor-mode 中一个名为 after 的参数有关,但是 define-minor-mode 传入的 after 参数写死了,被标记为了 after。所以今天我们的练习就是:定义一个自己的扩展宏,在原来 define-minor-mode 的基础上支持一个新的 :after 关键字。并且我们实现的思路是调用 define-minor-mode 时修改传递给add-minor-mode 中 after 的值,而不是简单的将 define-minor-mode 的源码拷贝过来修修改改,或者在调用完 define-minor-mode 之后手动的修改对应的顺序。虽然在日常使用中需要关注这个顺序的场景并不多,但是我想通过这个练习可以让读者更深入的理解lisp 中代码即数据的理念,以及将这种理念发挥到极致的宏的思想。这样读者在日后碰到像 use-package 这种复杂的插件不至于束手无策。为了使各位读者更容易理解这个思路,我们先从lisp 中代码即数据这理念讲起。首先我们要明确的一个概念是,Emacs中大多数内容都是一个list,同一个list既可以作为代码,也可以作为数据。例如 (message "Hello World")。我们既可以说他调用message函数并传入字符串参数,也可以说它是一个list,有两个成员: message符号,和字符串。既然它是list,所以我们就可以通过 car、cdr 这些操作list的函数来操作它。例如:(let ((sexp '(message "hello world")) (user "Jack")) (eval (list (car sexp) ;; message "Hello, %s" user)))这里我们将 (message "hello world") 这个list作为数据。后续我们根据这个数据,取第一个符号 message,后续跟上 "Hello, %s" 和 user 这个变量组织成一个新的list (message "Hello, %s" user)。最终我们调用 eval 执行这个list,最终得到了一个动态变化的代码。相当于我写代码修改了原来的代码。我们再来看看宏,实际上宏就是利用上述操作形成一组新的 list 数据,调用宏就是对宏生成的 list 执行 eval 操作。例如:(defmacro say-hello() '(message "Hello World")) (say-hello)因为我们只是想要宏展开生成的 list 数据,并不希望执行表达式,所以这里使用 ' 。而调用它的过程就是将其进行展开得到 (message "Hello World") 数据结构,并调用 eval 进行执行。得到宏展开的函数有 macroexpand 和 macroexpand-1 ,顾名思义,后者只展开一层,而前者会完全展开整个宏。因为这个宏也是写死了输出的内容,我们尝试一下实现一个新的宏,接受一个user参数,调用时输出 hello, user 字样。因为 say-hello 宏展开会得到一个list,所以我们只要操作这个展开的list,就能实现修改并添加参数的需求。(defmacro say-hello-to-user (user) (let ((sexp (macroexpand-1 '(say-hello)))) (list (car sexp);;message "hello, %s" user))) (say-hello-to-user "Tom")上述代码我们的思路是,通过 macroexpand-1 得到展开后的list数据,然后操作list,构建一个新的list并返回,在执行的时候执行这个list 就好了。这里提醒一下对宏不太熟悉的读者,我们在展开宏的时候需要使用 ' 或者 ` 表示这是一个引用不需要执行这个表达式。后面我们使用list 的办法是为了与前面的例子作为呼应减少理解难度,更地道的写法是(defmacro say-hello-to-user (user) (let ((sexp (macroexpand-1 '(say-hello)))) `(,(car sexp) "Hello, %s" ,user))) ;;使用 `表示后面,开头的表达式需要求值,最终也是生成一个 list 结构实际上我们完成上述需求也是这个思路,展开 define-minor-mode、在一堆list中找到我们想要的以 add-minor-mode 开头的list,修改list 中 after 对应位置的内容。解释如何改的问题,下面再看看如何解析 :after 参数。根据 define-minor-mode 的定义来看,它接受的关键字实际上是位于参数 body 位置的。body位置本身传入的也是一个list。对于 :keyword 这种形式的内容,是一组特殊的符号,在求值时永远返回它自己。关键字后面跟随的是它对应的具体的值。对于这种结构组成的list是一个 property list,我们可以使用 plist-get 和 plist-put 增加或者获取一个属性对应的值。我们要判断list中某个成员是否是这种形式的符号,可以使用 keywordp。所以说不管body中有哪些内容,他本质上还是一个list,仍然可以用操作list 的办法操作它。下面我们来看一个具体的例子:(defmacro say-hello (&rest body) (let ((user nil) ;; 存储user的值 (rest nil) ;; 存储剔除 user 项之后的表达式 (body-content body)) ;; 用于循环的临时变量 ;; 扫描 body,抽离 :user (while body-content (if (and (keywordp (car body-content)) (eq (car body-content) :user)) (progn (setq user (cadr body-content)) ; :user 后面的表达式 (setq body-content (cddr body-content))) (push (car body-content) rest) (setq body-content (cdr body-content)))) (setq rest (nreverse rest)) ; 前面的 push 打乱了原本的顺序恢复顺序 ;; 构造list, 作为宏的返回值 `(progn (message "Hello, %s" ,user) ,@rest))) (say-hello :user "Tom" (message "doing something else") (message "finish"))上面的代码结合注释不难理解,我们通过一个循环来提取我们关注的关键字 :user 之后的表达式。然后将剩余部分的代码整个组装并返回。大家看到这里对宏是否有了新的理解呢?宏本身就是一组操作list的固定代码,它将我们传入的数据或者代码进行打散重新组装,并输出我们想要的list组合。现在再来看 define-minor-mode 的代码可能就不那么难以理解了。到了这里相信各位读者对于本次我们要完成的功能应该知道如何下手了,甚至有的读者也跃跃欲试了,但是别着急,我们先来看看define-minor-mode 中有哪些代码我们可以借鉴的。首先我觉得值得聊聊的一处是开头位置 : (declare (doc-string 2) (indent defun) (debug (&define name string-or-null-p [&optional [¬ keywordp] sexp &optional [¬ keywordp] sexp &optional [¬ keywordp] sexp] [&rest [keywordp sexp]] def-body)))这段代码对于我们后续开发自己的宏很有借鉴意义。这段代码对于实际功能没有什么大的帮助,它们表达的含义如下:(doc-string 2) 表示宏的第二参数,也是就是 doc 参数是一个文档字符串,这样Emacs中的文档生成工具可以正确收集这个宏的说明文档(indent defun) 定义代码的缩进格式,按照函数的缩进方式进行debug 告诉调试器如何解析这个宏的语法结构因为我们要为自己的宏加上关键字 :after,我们看看 define-minor-mode 是如何处理这类参数的,有没有可以借鉴的: (while (keywordp (setq keyw (car body))) (setq body (cdr body)) (pcase keyw (:init-value (setq init-value (pop body))) (:lighter (setq lighter (purecopy (pop body)))) (:global (setq globalp (pop body)) (when (and globalp (symbolp mode)) (setq setter `(setq-default ,mode)) (setq getter `(default-value ',mode)))) (:extra-args (setq extra-args (pop body))) (:set (setq set (list :set (pop body)))) (:initialize (setq initialize (list :initialize (pop body)))) (:type (setq type (list :type (pop body)))) (:keymap (setq keymap (pop body))) (:interactive (setq interactive (pop body))) (:variable (setq variable (pop body)) (if (not (and (setq tmp (cdr-safe variable)) (or (symbolp tmp) (functionp tmp)))) ;; PLACE is not of the form (GET . SET). (progn (setq setter `(setf ,variable)) (setq getter variable)) (setq getter (car variable)) (setq setter `(funcall #',(cdr variable))))) (:after-hook (setq after-hook (pop body))) (_ (push keyw extra-keywords) (push (pop body) extra-keywords))))它的处理方式没有脱离我们上面介绍的处理框架。它也是遍历body列表,然后依次获取想要的内容。一口气将它不关心的参数放到了 extra-keywords 中。剩余的内容就被放到了 body 变量中。后面就是根据这些变量的值生成对应的list结构了。基于这些知识我们就可以编写需求对应的代码了:(defmacro my/define-minor-mode-ex (mode doc &rest body) "自定义的 define-minor-mode,支持额外传入 :after 关键字参数。" (declare (doc-string 2) (indent defun) (debug (&define name string-or-null-p [&optional [¬ keywordp] sexp &optional [¬ keywordp] sexp &optional [¬ keywordp] sexp] [&rest [keywordp sexp]] def-body))) (let ((after-eval nil) (clean-body nil) ;;剔除 after 之后的list (temp-body body)) ;;循环变量 ;;循环剔除 :after 关键字 (while temp-body (if (and (keywordp (car temp-body)) (eq (car temp-body) :after)) (progn (setq after-eval (cadr temp-body)) (setq temp-body (cddr temp-body))) (push (pop temp-body) clean-body) (push (pop temp-body) clean-body))) (setq clean-body (nreverse clean-body)) ;;展开原生的宏,得到对应的list数据 (let ((expand-code (macroexpand-1 `(define-minor-mode ,mode ,doc ,@clean-body)))) ;;如果指定了 :after 则修改里面调用 add-minor-mode 的参数 (when after-eval (setq expand-code (mapcar (lambda (form) (if (and (listp form) (eq (car form) 'with-no-warnings) (listp (cadr form)) (eq (car (cadr form)) 'add-minor-mode)) ;;修改参数列表 (let ((add-form (cadr form))) ;;获取参数列表 `(with-no-warnings (add-minor-mode ,(nth 1 add-form) ;; TOGGLE ,(nth 2 add-form) ;; NAME ,(nth 3 add-form) ;; KEYMAP ,after-eval ;;这里就是我们要替换的 after 参数 ,@(nthcdr 5 add-form)))) ;;剩余的一口气全塞进去 form)) expand-code))) expand-code))) (my/define-minor-mode-ex my/focus-mode "test mode" :after 'bar-mode)到此我们的任务就完成了,这个是基于当前宏展开的,我们是按照 with-no-warnings 在顶层list中可以遍历到,并且 add-minor-mode 是 with-no-warnings 所在列表的第二个参数来编写的。如果后续它们的顺序改变了,这段代码可能就不那么准了,如果要严谨一些的话,我们可以递归遍历所有列表的子元素进行查找。这个读者可以自行尝试,我们就不在这里再展开了。
2026年09月15日
7 阅读
0 评论
0 点赞
2026-09-14
《elisp 每日一练》—— Body 代码块
前面我们介绍了 define-minor-mode 中的init-value、global、keymap、lighter 关键字,了解了它对于开关变量、快捷键和 mode line 显示上的管理。现在我们进入关于 define-minor-mode 的最后一个步骤,看看如何利用body管理minor mode 中自定义的资源背景知识我们来编写一个简单的测试代码(define-minor-mode my/focus-mode "" :lighter "My focus" (message "welcome to my focus mode"))展开之后,我们发现它被包裹在开关函数之中,无条件的被执行。根据我们前面介绍的该宏的几个核心动作,它位于维护 local-minor-modes 之后,但是在执行 hook 变量之前。所以我们在编写这部分代码时通常要先判断当前mode是处于卸载还是加载的状态。一个良好的 Minor Mode 应该不影响其他buffer,关闭和开启时也没有残留,所以我们这部分代码一定要重视资管的管理。Emacs中主要的资源有变量、hook、定时器、以及一些ui特效。对于它们的管理一般的规范如下:对于变量,尽量使用 setq-local 避免污染全局,同时在退出时记得将它还原成原来的值对于 hook 来说,add-hook 时尽量使用 buffer local 的 hook,在关闭当前mode时要记得卸载hook对于timer 来说, 定义也应该使用 setq-local,关闭时记得关闭在UI上面,开启时应该保存原值之后再修改,关闭时恢复原始模样练习本节的内容比较简单,因此我们的练习也比较简单,我们定义个简单的mode,用于改变当前buffer的一些UI特性,我这里简单的将它的特性定义为:显示行号,并且显示相对行号调整光标样式为一条细竖线标记光标所在行我们需要管理这些属性,以便退出mode 时它能正常恢复(define-minor-mode my/normal-notepad-mode "修改Emacs的显示效果" :lighter " LD" (if my/normal-notepad-mode (progn ;; 保存当前值(buffer-local 保存) (setq-local my-line-display--saved-line-numbers (if (bound-and-true-p display-line-numbers) display-line-numbers-type 'off)) (setq-local my-line-display--saved-hl-line (if (boundp' hl-line-mode) hl-line-mode global-hl-line-mode)) (setq-local my-line-display--saved-cursor-type (if (boundp 'cursor-type) cursor-type t)) ;; 应用新设置 (setq-local display-line-numbers-type 'relative) (display-line-numbers-mode 1) (hl-line-mode 1) (setq-local cursor-type 'bar)) (progn ;; 恢复行号 (if (eq my-line-display--saved-line-numbers 'off) (display-line-numbers-mode -1) (setq-local display-line-numbers-type my-line-display--saved-line-numbers) (display-line-numbers-mode 1)) ;; 恢复高亮行 (unless my-line-display--saved-hl-line (hl-line-mode -1)) ;; 恢复光标 (setq-local cursor-type my-line-display--saved-cursor-type))))上面的套路非常简单,先判断当前状态,如果处于开启状态下,我们可以先保存原始值,再进行覆盖。如果关闭我们使用原来保存的值进行恢复。需要注意的是,因为我们仅仅关注当前buffer的显示,所以这里我们应该使用 setq-local 定义局部缓冲区变量。另外要保存未启用时的原始值,我们只能在开启状态,设置这些属性之前进行处理,否则我们取到的就不是原始值。小结本篇我们的内容非常简单,我们主要提供一个代码为大家演示了如何正确获取原始值并在关闭mode的时候恢复原始状态。
2026年09月14日
5 阅读
0 评论
0 点赞
2026-09-13
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
前面几篇我们陆陆续续的分析了 define-mode-line 中管理开关变量、快捷键相关的内容。我们可以通过 init-value、global来设置开关变量的初始值和是否定义为全局启动。通过 keymap 可以定义快捷键的keymap。现在我们来讨论 lighter 关键字的相关内容背景知识mode-line-formatlighter 关键字是用来定义当 Minor Mode 激活时在 Mode Line (状态栏) 上显示的文本。在详细描述 lighter 的作用之前,我们要先来讨论 Mode Line 的显示机制Emacs 的 Mode Line 位于每个窗口底部, 在未经任何美化的Emacs中,它位于 minibuffer 上方显示为灰色,用于显示缓冲区状态和模式信息。它的显示内容由 buffer local 变量 mode-line-format。这是一个由列表、字符串、符号和数字构成的递归数据结构。我在Windows上打开的 *scratch* buffer 中,它的显示的内容为 c\--- *scratc* All L4 (Lisp Interaction ElDoc)c 表示当前缓冲区的编码系统是c, 通常对应 utf-8 或 undecided 在中文环境下的显示,具体取决于你的 buffer-file-coding-system\ 表示换行符的类型是 Dos 风格,也就是采用 \r\n 来换行,对于unix 类型的换行,它显示为 :,Mac风格的换行符显示为 /第一个 - 表示当前缓冲区是可写的,对于不可写的通常表现是 %第二个 - 表示当前缓冲区未被修改,修改之后显示为 *第三个 - 表示是在本地存储,如果是远程的会显示为 @*scratch* 表示当前buffer 的名称ALL 表示当前窗口显示了整个buffer,如果buffer 内容较多,一个窗口显示不下的话,这里可能是 TOP表示显示的是开头位置,BOTTOM 表示显示的结尾,又或者是一个百分比L4 表示光标在第四行Lisp Interaction ElDoc 表示当前加载的 Major Mode 是 Lisp Interaction 、Minor Mode 是 ElDoc这些东西其实不用记忆,因为Emacs最大的特点是灵活,你不喜欢这些显示内容完全可以自己修改,显示自己想要的信息。我们可以通过调整 mode-line-format 变量的值来修改显示内容。我们查看当前 *scratch* buffer 中 mode-line-format 的值如下:(%e mode-line-front-space (:propertize (mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-window-dedicated) display (min-width (6.0))) mode-line-frame-identification mode-line-buffer-identification " " mode-line-position (project-mode-line project-mode-line-format) (vc-mode vc-mode) " " mode-line-modes mode-line-misc-info mode-line-end-spaces)这个例子很好的演示了 mode-line-format 中的几种常见数据类型:符号例如 mode-line-front-space 、mode-line-buffer-identification 、mode-line-position、mode-line-modes 等等都是一个符号,对于符号,Emacs会取出它的值,这些值可以是列表、字符串、或者数字,后面会再把它当作新的结构递归处理。字符串它可以是固定的字符串,也可以以 % 开头的格式化内容,例如 %b 会显示buffer 名称、%f 显示对应的文件名。这些内容读者可以查看Emacs中关于 mode-line-format 的文档。我们这个例子中的字符串仅仅是几个空格列表整个 mode-line-format 就是一个列表,列表的处理规则取决于第一个元素。如果第一个元素是字符串或者另一个列表,则递归处理所有元素并拼接。% 构造这里开头位置是一个 %e ,虽然说字符串中也可以使用 %e ,但是这里显然不在字符串中。这里的 %e 表示当前缓冲区文件名或进程状态的简短表示。:propertize:propertize 构造规则是: 先递归处理第一个参数(这里是包含多个符号的列表),然后给结果加上文本属性。这里加的是 display 属性,值为 (min-width (6.0)),意思是这段内容至少占 6 个字符宽度。这解决了编码信息(如 c\)在窗口窄时被截断的问题。条件构造条件构造的语法规则是 (symbol then else) , (project-mode-line project-mode-line-format) 就是一个条件构造,如果 project-mode-line 是非 nil 则,处理 project-mode-line-format。后面的 vc-mode 也是同理,它表示,如果当前buffer 开启了 版本管理,就显示版本管理的信息。读者可以打开一个git管理的项目,看看显示效果根据上面的介绍,我们可以推测出上面 mode-line-format 的值将会如何显示:%e 是文件名/进程状态简短标记mode-line-front-space 前端空白,通常为空或一个空格:propertize 它是一个结构,你可以理解为一组信息。根据后面的符号我们知道它分别显示: 编码、客户端、修改状态、远程、专用窗口标记,并且根据后面的属性定义可以知道,它保证至少 6 字符宽mode-line-frame-identification 帧标识,在我的Emacs scratch buffer中显示为空mode-line-buffer-identification 缓冲区名称mode-line-position 光标所在位置project-mode-line 如果启用 project-mode 则通过 project-mode-line-format 显示项目相关的信息vc-mode 如果开启了版本管理,显示版本管理相关的信息mode-line-modes 显示当前开启的mode 信息mode-line-misc-info 其他信息mode-line-end-spaces 尾部空白,通常用于右对齐或者填充一般来讲,如果我们希望修改显示的内容,可以修改其中的某个符号。或者我们可以往 mode-line-misc-info ,这个是官方预留给我们用来自定义显示内容的接口。但是我们修改之后Emacs不会立马启用,需要调用 (force-mode-line-update) 让本次变更在下次重绘时生效。lighter(define-minor-mode my/focus-mode "" :lighter "Be Focus")展开之后,我们发现实际上与 Mode Line 显示相关的内容主要操作是:(add-minor-mode 'my/focus-mode '"Be Focus" (if (boundp 'my/focus-mode-map) my/focus-mode-map) nil nil)通过将 :lighter 中指定的内容传入 add-minor-mode 作为参数然后在切换状态的函数 my/focus-mode 最后调用 (force-mode-line-update) 刷新显示状态。我们再进入 add-minor-mode 中查看关于 Mode Line 显示信息的相关内容:(when name (let ((existing (assq toggle minor-mode-alist))) (if existing (setcdr existing (list name)) (let ((tail minor-mode-alist) found) (while (and tail (not found)) (if (eq after (caar tail)) (setq found tail) (setq tail (cdr tail)))) (if found (let ((rest (cdr found))) (setcdr found nil) (nconc found (list (list toggle name)) rest)) (push (list toggle name) minor-mode-alist))))))我们来分析一下,它是如何将我们的内容显示到 mode line 上的。这段代码与昨天我们分析 keymap 时的行为类似,它是通过修改 minor-mode-alist 来修改 mode line 的显示内容。我们发现它在插入时仍然与 after 有关,默认它会尝试插入到 after 之后。我们前面介绍过,默认的 mode-line-format 中通过 mode-line-modes 显示开启的mode信息。我们查看 mode-line-modes 的值可以找到这样一段代码:(:propertize (#1= minor-mode-alist) ; 要显示的 construct mouse-face mode-line-highlight ; 鼠标悬停时高亮 help-echo "Minor mode mouse-1: Display minor mode menu mouse-2: Show help for minor mode mouse-3: Toggle minor modes" local-map (keymap (header-line keymap (down-mouse-3 . #4=(menu-item "Menu Bar" #3# :filter bindings--sort-menu-keymap))) (mode-line keymap (down-mouse-3 . #4#) (mouse-2 . mode-line-minor-mode-help) (down-mouse-1 . mouse-minor-mode-menu))))这里我们又遇到了 :propertize 。我们还是一样,将第一个元素 (#1= minor-mode-alist) 作为显示的内容,第二个元素mouse-face mode-line-highlight 作为属性,表示这部分显示的内容鼠标放在上面要高亮。后面的 help-echo 表示鼠标放在上面弹出的提示信息。我们来看看 (#1= minor-mode-alist) 表示的含义。前面的 #1= 表示为 #1 。后面可以使用 #1# 来引用。这个内容在前面已经被引用了,所以说这里实际上将 minor-mode-alist 的值标记为 #1 并且进行了显示,后面的内容就是显示的属性。我们可以将鼠标放在上面显示提示信息。练习本节的练习,我们围绕着修改 mode line 的显示进行,我们定义一个 minor mode ,它可以统计buffer中文字数量并显示在 mode line 中。我们先定义一个统计函数(defun my/count-words() (save-excursion (goto-char (point-min)) (let ((count 0)) (while (forward-word 1) (setq count (1+ count))) count)))我们可以在lighter 中使用 eval 构造,这里我们要在后续动态更新的单词数量,所以我们不能使用 (format "%s" my/count-words) 这种形式。我们在前面看到 lighter 中的内容会被原样传入到 add-minor-mode 中,相当于先格式化一个字符串,然后将字符串作为 name 加入到 minor-mode-alist 作为后续显示。后面实际上我们就无法实时更新了。所以这里最好由 eval 来构造一个结构,每次刷新时由Emacs自己计算。(define-minor-mode my/count-words-mode "Count Words" :lighter (:eval (format " WC:%d" (my/count-words))) :init-value t)这样我们发现,每次我们输入一个新的单词之后,在mode line上都会更新显示。我们每一次输入都伴随着显示的刷新,Emacs每次刷新都会根据 mode-line-format 中的内容实时计算,每次计算时因为我们传入的是一个 eval 构造,所以每次都会调用 my/count-words 显示它的结果。总结本章的内容我们先介绍了 mode-line-format ,它是一个列表,Emacs每次更新显示都会解析列表并根据列表计算的值来更新 mode line 显示。我们通过 define-minor-mode 中的 :lighter 定义的内容会通过调用 add-minor-mode 被追加到 minor-mode-alist 中。而 mode-line-format 中存在一个名为 mode-line-modes 的符号,它也是一个列表,内部会通过解析 minor-mode-alist 来实现 minor mode 的名称。
2026年09月13日
7 阅读
0 评论
0 点赞
2026-09-11
《elisp 每日一练》——minor mode 开关变量的管理
上一节我们将定义 minor mode 的宏 define-minor-mode 展开并分析了它的作用。它主要是定义用于切换mode启用开关的变量和函数,以及在切换时进行模式的管理和hook的切换。默认ninor mode 创建时处于管理状态,而且minor mode启动时只对当前buffer生效。本节我们来讨论如何通过控制开关变量来改变这两点。背景知识:init-value在介绍之前,我们补充一个知识点。那就是 define-minor-mode 的 :init-value 关键字。我们还是将下面的定义展开,看看与默认的相比多了什么部分的代码(define-minor-mode my/focus-mode "This is a focus mode" :init-value t)展开之后得到的代码实在是太多了,我们就不一一介绍了。我们只看关键的部分(defvar-local my/focus-mode t)默认定义时,切换开关的变量被定义成了 nil 也就是关闭状态。现在被设置成了打开。其他的部分没有什么大的变化。所以据此我们可以得出结论 :init-value 用来控制定义时是否打开对应的mode。customize 机制现在我们正式开始讨论 Customize 机制。Customize 机制是Emacs提供的一套配置UI框架,任何人都可以通过对应的函数将对应的变量作为配置通过UI界面显示内容并进行设置。我们可以通过 customize 函数打开该界面。我们通过界面设置的值被保存到一个文件中,该文件由Emacs自己维护,默认放在我们的主配置文件中。我们可以通过 custom-file 设置这些值放置的位置。或者通过 custom-file 函数获取custom file配置文件的位置。我们通过 customize 设置的值被放在 custom file 包裹在 custom-set-variables 函数中。我们可以通过 defcustom 宏定义一个能被customize 机制显示和管理的宏。(defcustom SYMBOL STANDARD DOC &rest ARGS)其中 symbol 是一个符号,表示一个 customize 类型的变量。STANDARD 表示默认值。DOC 表示变量得说明文档。ARGS 是一组由关键字组成得属性定义。常见得属性有::type 变量得数据类型,有 'string、'integer、'boolean '(choice ...):set 是一个函数,当变量被赋值时会调用这个函数,通过函数来给变量赋值,而不是直接赋值。在函数中需要使用 setq-default 来为变量重新设置值,setq 也会触发该函数,容易引起递归调用:version 该标记用来提醒用户,这个变量得使用适用于哪个版本,可以显示Emacs版本或者对应 package 版本:tag 覆盖显示界面中得变量名,用于给用户一个更好的提示:link 在 Customize 缓冲区中显示一个可点击的链接,通常指向相关文档或 info 节点:group 指定变量所属的自定义组。这将决定它在customize 树中显示的位置,一个变量可以属于多个组Emacs 中customize group 作用是像文件目录一样,把成千上万的用户选项按类收集整理方便用户查看。Emacs 中有一些常用的默认组可以使用:Editing:基础文本编辑功能相关的选项Convenience: 用于加快编辑功能的快捷选项External: 与外部工具交互的组我们也可以自定义自己的组,自定义组使用 defgroup ,它的签名如下:(defgroup SYMBOL MEMBERS DOC &rest ARGS)SYMBOL 表示自定义组的名称、MEMBERS 表示组中的成员、ARGS 也是一个由关键字组成的属性定义。常用的属性有::group 父组:prefix 变量名前缀建议其他的属性含义则与 defcustom 的相同。现在我们来看看 define-minor-mode 是如何管理我们的customize 变量的。(defgroup my-focus-group nil "My focus group" :group 'tools :prefix "my-focus-") (define-minor-mode my/focus-mode "This is my focus mode" :init-value nil :type 'boolean :global t)我们展开这个宏,发现它将我们的切换开关变量改为了 (defcustom my/focus-mode nil),也就是它将变量设置成了一个customize 变量。如果我们在 gloabl 为 t 的情况下指定了 group 那么,它此时就是该组中的一个变量。为什么只有gloabl 为 t 时才会设置为customize 变量呢,因为 minor mode 是挂载到单个buffer上的,它依附与buffer,所以默认情况下minor mode 的切换开关也是一个 buffer local 变量,如果我们希望切换对全局都有效。为了配置这种一处修改所有buffer都启用,为了管理这种情况,宏也在其他地方进行了适配(when (boundp 'global-minor-modes) (setq global-minor-modes (delq 'my/focus-mode global-minor-modes)) (when (default-value 'my/focus-mode) (push 'my/focus-mode global-minor-modes)))我们发现,它现在管理的不再是 local-minor-modes 而是全局的 global-minor-modes。另外需要注意的是,既然我们的 minor mode 打开了 global 开关变成了全局可用的,那么为了一致性,mode 中使用的相关变量也尽量使用全局变量,如果使用 buffer local 变量会导致有的buffer有这个变量,而有的没有,或者不同buffer中同名的变量值不同,导致行为不一致。如果我们希望定义customize 变量来控制minor mode 的一些行为,那么还是要回归到自己利用 defcustom defgroup 定义组。练习我们来定义一个提醒休息的小功能,它的作用是每隔一段时间就提醒我们休息一下,我们没有学定时器相关的内容,所以只能做一个手动触发的功能。无法(defgroup my-timer-group nil "自定义定时提醒模式的配置组" :group 'convenience :prefix "my-timer-") (defcustom my-timer-message "时间到!休息一下吧" "定时提醒时弹出的文本" :type 'string :group 'my-timer-group) (defcustom my-timer-interval-seconds 30 "提醒时间间隔 (单位:秒)" :type 'integer :group 'my-timer-group) (defcustom my-timer-enable-sound nil "是否播放提示音" :type 'boolean :group 'my-timer-group) (define-minor-mode my-timer-mode "定时提醒" :init-value nil :group 'my-timer-group) (defun my-timer-show-message() (interactive) "显示提示信息" (let* ((buffer (get-buffer-create "MyTimer")) (show-message (message "你已经坚持了%d, %s" my-timer-interval-seconds my-timer-message))) (with-current-buffer buffer (erase-buffer) (goto-char (point)) (insert show-message)) (display-buffer buffer)))小结本节我们讨论了minor mode 中是如何通过 :init-value 、 :global 和 :group 来管理开关变量。简单来说就是默认创建的 Minor Mode 都是关闭状态,想要创建时启动可以通过 :init-value 将它设置为 t,如果希望启用之后全局生效可以 设置 :global 为 t,它会将变量定义为 customize 中的变量,在这种情况下我们可以通过 :group 为它设置一个组。
2026年09月11日
7 阅读
0 评论
0 点赞
2026-09-10
《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(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. 运行hookhook 的概念我们还没有开始接触,但是如果读者对其他编程语言熟悉,相比对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-modeNAME 为 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 之类的关键字看看它们在背后都做了什么
2026年09月10日
7 阅读
0 评论
0 点赞