首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
287 阅读
2
从零开始配置 vim(15)——状态栏配置
169 阅读
3
nvim番外之将配置的插件管理器更新为lazy
159 阅读
4
2018总结与2019规划
145 阅读
5
PDF标准详解(五)——图形状态
121 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
emacs
windows
elisp
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
442
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
442
篇与
的结果
2026-09-22
《elisp 每日一练》——管理 syntax-table
在前面介绍buffer 的时候,简单的介绍了 syntax-table的概念。它是一个存储了 ASCII/Unicode 字符与其语法类 (Syntax Class) 对应关心的表格。在前面的文章中,我只是介绍了如何使用它,例如我们介绍了 scan-sexps 函数,利用表中数据在语法单元间移动。而如果我们要创建自定义的 Major Mode,核心工作之一就是维护自己的 syntax-table。背景知识syntax class前面提到:它存储的是字符到语法类的对应关心。我们先来介绍一下常用的语法单元 (Syntax Class)。常用的有:语法类名称字符代码含义与示例空白符' ' 或 '-'空白字符(空格、Tab、换行符)单词组成字符'w'构成普通单词的字符(字母、数字)。例如按 M-f 会穿过连续的 w符号组成字符'_'构成标识符/符号的字符(如 _ 或 -)。foo_bar 中 _ 设为 _ 时,它是一个完整 Symbol标点'.'标点符号(如 .、 ,、 ;)括号'(' / ')'成对出现的开/闭括号(如 ()、[]、{})字符串引号" '"字符串分界符(如 "" 或 ''转义字符'\'转义字符(如 \)注释起始符< >界定注释的起始和结束位置维护syntax-table 的一般操作syntax-table 与一般的数据一样,对它的操作无外乎增删改查。我们还是按照这个思路来介绍。首先我们可以使用 make-syntax-table 来创建一个空表。根据我们前面在分析 define-derived-mode 中看到的,它一般被定义为全局变量。但是在实际开发中,一般不会从0开始,而是继承自某个父类,以此为基础进行个性化改造。对于编程语言,可以从 prog-mode-syntax-table 继承,而普通文本可以从 standard-syntax-table 。在继承之后,针对一些不合适的项目,我们可以使用 modify-syntax-entry 设置字符的语法类。它的函数原形是:(modify-syntax-entry CHAR NEWENTRY &optional SYNTAX-TABLE)CHAR 是一个字符常量,也可以是一个 (MIN . MAX) 的范围,表示该范围内的字符都是这个Syntax ClassNEWENTRY 是语法描述符,是一个字符串,它每个字符都有明确的含义。第一个字符是前面介绍的Syntax Class 符号;第二个参数是匹配字符,仅当第一个字符是 ( 或者 ) 才有意义;第三个及之后的字符是语法标志,它是一组数字,用于描述多字符注释、可嵌套注释等高级行为。标志的顺序无关紧要。SYNTAX-TABLE 要修改的语法表,如果不填默认修改当前buffer中的syntax-tableNEWENTRY 详解NEWENTRY 是一个相对复杂的数据,我们来通过简单的例子来说明一个字符的情况(modify-syntax-entry ?# "<" my-table)上面的代码,我们设置 # 字符为 shell 的注释。这段代码中,NEWENTRY 只有一个字符,表示 Syntax Class括号的处理(modify-syntax-entry ?\( "()" my-table) (modify-syntax-entry ?\) ")(" my-table)这段代码,我们设置了一个括号区域,它的 NEWENTRY 域有两个字符,第一个字符 ( 表示这是一个左括号,而第二个表示与它匹配的右括号是 ),所以如果我们要添加一个描述中括号的规则可以这么写(modify-syntax-entry ?\[ "(]" my-table) (modify-syntax-entry ?\] ")[" my-table)注释C 语言有两种注释符号,我们看看该如何在 syntax-table 中定义它们。首先我们来看看 /**/(modify-syntax-entry ?/ ". 14" my-table) (modify-syntax-entry ?* ". 23" my-table)既然 /* 是注释,为什么不用代表注释的 < 来设置呢,主要是因为 / 在C 语言中主要的含义是除法运算符,只有和 * 在一起的时候才作为注释。所以Emacs的做法是将它作为运算符处理。作为注释的作用则通过语法标记来完成。Emacs中用于语法标志的数字含义为:1:该字符是双字符注释起始符的第一个字符。2:该字符是双字符注释起始符的第二个字符。3:该字符是双字符注释结束符的第一个字符。4:该字符是双字符注释结束符的第二个字符。对于 /**/ 这样的注释来看,/ 它位于注释区域的第一个字符作为开始,注释的结束位置它是第4个字符。而 * 则对应于注释区域的第2和第三个字符。那么根据这个标准,我们可以定义xml的注释 <!---->, 理论上我们可以依葫芦画瓢写成如下模式:在xml中,注释是 <!----> 是否可以写为 (modify-syntax-entry ?< ". 1" my-table) (modify-syntax-entry ?! ". 2" my-table) (modify-syntax-entry ?- ".3456" my-table) (modify-syntax-entry ?> ".7" my-table)但是遗憾的是,上述方案并不能正常工作。因为Emacs中对于syntax class ,它只支持两个字符,也就是语法标志只支持从1到4。对于这种复杂的类型,可以使用 syntax-propertize-function 函数,通过正则表达式来支持。说完了 /**/,我们再来说另一种注释 //, 根据前面我们的描述,/ 本身是运算符,所以作为注释也是使用数字标识,那么它的写法如下:(modify-syntax-entry ?/ ". 12" my-table)有的读者看到这里是不是认为上述写法没问题呢?// 作为注释它出现在第一和第二位置。如果你这么想,你可能只对了一半,作为 // 类型的注释,它确实出现在 1 4 两个位置,但是别忘了,/ 还可以以 /**/ 的形式出现。所以正确的写法应该是结合我们前面介绍的 /**/ 类型处理方式:(modify-syntax-entry ?/ ". 124" my-table)字符串常量的处理对于字符串常量来说,一般以 "" 进行分隔,它即作为字符串的开始也作为结束的标志,所以我们不需要像前面的括号那样专门制定一个闭合的字符。一般来讲,可以写下如下的代码来处理(modify-syntax-entry ?\" "\"" my-table)这样就够了吗?字符串还有很多复杂的变种,例如对于C++ 11以后的标准,它允许使用 R("") 来表示一个字符串。有的字符串里面采用 \" 表示字符串内部有" 这个字符。复杂的今天我们就不讨论了,我今天只处理 \ 作为转义字符的情况,可以写下如下代码处理:(modify-syntax-entry ?\\ "\\" my-table)小结本节的内容就结束了, 本节我们主要介绍了 modify-syntax-entry 函数,并且通过几个例子演示了它的使用方法。其实可以归纳为3类:单字符就是一个 syntax class 的,直接使用单字符字符串指定对于需要不同字符表示一个区间的,可以使用两个字符的字符串指定,第一个字符是 syntax class 类型,第二个字符表示闭合字符对于4个字符组成的注释区间,可以使用数字 1 2 3 4 指明作为注释符号时它可以出现的位置
2026年09月22日
0 阅读
0 评论
0 点赞
2026-09-21
《elisp 每日一练》—— Major Mode mode line 显示
在前面我们介绍了 Minor Mode 是如何在mode line中显示的。简单来说就是 mode line 的显示依靠 mode-line-format,默认的format 中存在一个名为 mode-line-modes 的符号,它会解析并显示 minor-mode-alist 的内容。在定义 minor mode 的时候通过关键字 lighter 传入的文字在调用 add-minor-mode 时会被加入 minor-mode-alist 中,在mode处于启用状态时显示。在 define-derived-mode 中没有这样的关键字,它是直接通过宏参数赋值给 mode-name 进行显示。今天我们来看看背后发生了什么背景知识我们以 scratch buffer 为例。在Emacs中可以查到 mode-name 的值为 "Lisp Interaction"。前面在讨论 mode-line-format 的时候提到过,当前加载mode的信息位于 mode-line-modes 变量中,我们查看该变量找到关键信息(在最开始的位置):(:propertize (#1="" mode-name) help-echo "Major mode mouse-1: Display major mode menu mouse-2: Show help for major mode mouse-3: Toggle minor modes" mouse-face mode-line-highlight local-map (keymap ...))根据我们前面介绍的 mode-line-format 格式,#1 是一个引用标记,等价于空字符,所以这部分显示的就单单只是 mode-name 的值练习各位读者在配置Emacs或者vim的 lsp 的时候会发现,一些插件会显示lsp 服务器的状态例如显示 lsp[connected] 字样表示已经连接上了lsp服务器。我们来尝试一下利用 mode-line-format 来模拟一下这个状态。首先我们定义一些模拟函数(defvar my/lsp-test-status nil "模拟 LSP 连接状态。取值如 nil、'connected。") (defun my/test-lsp-connect () (interactive) (setq my/lsp-test-status 'connected) (force-mode-line-update)) (defun my/test-lsp-disconnect () (interactive) (setq my/lsp-test-status nil) (force-mode-line-update))然后我们还需要一个根据状态查找显示字符串的函数:(defun my/test-lsp-status () "根据 `my/lsp-test-status' 返回要显示的字符串。" (pcase my/lsp-test-status ('connected " lsp [connected]") (_ "")))最后我们定义一个对应的 Major Mode。根据文档,NAME 变量本身应该传入一个字符串,所以我们实际上不能直接在 NAME 字段输入列表作为显示内容,而应该在外部进行修改。(define-derived-mode my/lsp-test-mode nil "Test" "演示动态 mode-name。" (setq mode-name '(:eval (concat "Test" (my/test-lsp-status)))))总结本节的内容比较简单,实际上读者在阅读并理解了之前分析 minor mode 中显示的文章之后,本节内容差不多能无师自通了。唯一需要注意的是 NAME 参数在文档中已经明确说明是字符串。所以如果想要实现动态更新的话,可能要在后面修改 mode-name 的值。
2026年09月21日
0 阅读
0 评论
0 点赞
2026-09-20
《elisp 每日一练》—— Major Mode 管理 abbrev-table
在前一篇博文中,我们讨论了Emacs中 abbrev-table 的基础用法,现在我们来看看 define-derived-mode 是如何管理 abbrev-table 资源的背景知识我们先来回顾一下之前默认展开时它是如何处理 abbrev-table(progn (defvar my/code-mode-abbrev-table) (unless (boundp 'my/code-mode-abbrev-table) (put 'my/code-mode-abbrev-table 'definition-name 'my/code-mode) (defvar my/code-mode-abbrev-table (progn (define-abbrev-table 'my/code-mode-abbrev-table nil) my/code-mode-abbrev-table))) (unless (get 'my/code-mode-abbrev-table 'variable-documentation) (put 'my/code-mode-abbrev-table 'variable-documentation (purecopy "Abbrev table for `my/code-mode'."))))首先通过 defvar 定义了一个以 mode 名称加上 -abbrev-table 形式的变量名。根据 defvar 的性质,如果我们在外部有这么一个名称的变量,那么它会使用我们定义的变量作为 Major Mode 的 abbrev-table 进行管理。后续利用 boundp 如果发现该变量名没有绑定值,那么它会自己使用我们在上一节中介绍的函数 define-abbrev-table 定义一个。另外的一些代码就是设置一些属性用于给对应的交互式工具展示信息。在mode函数中,主要的代码如下:(unless (or (abbrev-table-get my/code-mode-abbrev-table :parents) (eq my/code-mode-abbrev-table local-abbrev-table)) (abbrev-table-put my/code-mode-abbrev-table :parents (list local-abbrev-table)))这段代码仅仅通过我们前面介绍的 PROP属性中的 :parents 设置了继承关系。根据上面的代码,我们可以得出结论,默认情况下,它是以 mode 名称加上 -abbrev-table 形式的变量名 作为当前Major Mode 的 abbrev-table 进行管理,并且如果在定义时设置了继承关系,那么它会通过 :parents 属性设置 abbrev-table 的继承关系。实际上,define-derived-mode 也支持通过 :abbrev-table 关键字来进行abbrev-table 的管理,我们同样的以一个例子来展开讨论:(define-derived-mode my/code-mode prog-mode "MyCode" "继承自 prog-mode 的代码主模式" :abbrev-table (defvar major-mode-abbrev-table (define-abbrev-table 'majro-mode-abbrev-table nil "自定义 Major Mode 的 abbrev-table")))我们展开之后会发现,原来有关 abbrev-table 的一切代码都消失了,仅仅留下了(setq local-abbrev-table (defvar major-mode-abbrev-table (define-abbrev-table 'majro-mode-abbrev-table nil "自定义 Major Mode 的 abbrev-table")))我觉得可能是开发者觉得既然我们已经通过 :abbrev-table 指定了外部的 abbrev-table,那么对于继承和创建一定有自己的理解,就不需要我接管了。所以这里仅仅帮我们把当前 local-abbrev-table 变量指向了我们自己定义的 abbrev-table。所以这就给我们提了一个醒,使用 :abbrev-table 实际上是比较危险的行为,作为一个菜鸟,在不具备深入理解 abbrev-table 机制的情况下贸然使用 :abbrev-table 关键字可能会带来意想不到的问题。所以在处理 abbrev-table 的时候,最好的办法就是定义一个类似名称 abbrev-table 变量,我们只关心要往里面加入哪些对应的映射关系,而不考虑其他,其他内容由 define-derived-mode 自己处理。小结本节我们总结了 define-derived-mode 是如何管理 abbrev-table 的。如果不指定 :abbrev-table 关键字,它会默认将 mode 名称 + -abbrev-table 名称的变量作为当前Major Mode 的 abbrev-table 进行管理,主要操作是:设置文档属性,方便后续用户通过交互式窗口查看它的相关信息如果没有定义,默认定义一个空表设置 :parents 的继承关系更新当前 local-abbrev-table 设置而如果我们使用了 :abbrev-table 关键,它只会更新 local-abbrev-table 设置。也就意味着一切由我们自己处理,一切问题自己承担。所以在处理 abbrev-table 最好的办法是不使用 :abbrev-table 关键字,我们只关系表中的映射关系,其余的交给 define-derived-mode 处理。结合我们昨天提到的 edit-abbrevs 编辑方式,我们完全可以放心的把 abbrev-table 交由 define-derived-mode 自己处理
2026年09月20日
1 阅读
0 评论
0 点赞
2026-09-19
读《封神演义》
以前我只听过封神的故事:哪吒脑海、纣王制作炮烙虿盆酷刑、姜太公垂钓、武王伐纣等等,这些故事都是从小看各种动画片和电视剧知道的,封神的原著一直没看过,最近一段时间我看完了整本封神演义。封神演义的故事说来也简单,全书以武王伐纣、商周易代的历史为框架,叙写天上的神仙分成两派卷入这场斗争,支持武王的阐教,帮助纣王的为截教。双方祭宝斗法,几经较量,最后纣王失败自焚,姜子牙将双方战死的要人一一封神。这本书的观感还是挺奇妙的:彻底颠覆了我对哪吒的映像:从小看的各种故事书,动画告诉我哪吒是一个反抗压迫,具有斗争的精神的人。但是整本书看起来他就是一个患有超雄综合征的熊孩子。小时候在东海洗澡用混天绫搅得东海不得安宁,而哪吒后来不由分说打死夜叉、打死龙王太子。后续又拉起轩辕弓射杀石矶门童,各种闯祸看的人血压飙升。太乙真人也不是什么正义形象,在哪吒闯祸之后只因天数规定哪吒后续是辅佐周氏伐纣的先锋,因此对哪吒一味纵容,龙王上天告状,太乙真人给哪吒隐身法宝,重伤龙王。但是太乙真人认为比起封神大事来说只是无关紧要的小事。甚至在哪吒射杀石矶门童,导致石矶意欲报复哪吒的时候太乙真人现身收了石矶,这在我看来实在是有点不可理喻。似乎所有人的不甘与痛苦都需要为伐纣封神这种天数让路。阐教的人虽说支持武王,顺应天时。但是于上面的太乙真人一样,为了天数可以牺牲任何人。最让人不理解的是诛仙阵,也不知道是为了突显诛仙阵的险恶还是封神榜为了凑人数,燃灯真人的做法是先拍炮灰送死,然后安排能破阵的人破阵。明眼人都能看出来诛仙阵的险恶,还安排没有道术、或者道术不过关的普通人送死,而所谓的十二金仙却稳坐钓鱼台,实在是令人费解。另一件让人觉得可惜的就是邓婵玉因为阐教所谓的姻缘被迫嫁给土行孙,有点水浒传中扈三娘强配给王英的既视感。或许在古人眼里这种婚配很正常,但是以我现代人看来实在有点荒唐。姜子牙也没有我想象中那么神通广大。书中多次从敌人口中提到姜尚善于用兵,但是我从书中看到遇到困难姜子牙从来没有自主客服过。要么上昆仑山、要么靠杨戬。可以说姜子牙的角色换一个不知名的人物都可以顺利完成,但是万万不能缺了杨戬。书中虽然有神仙斗法,但是场面太多单一,对比三国演义各种计谋,各种战略,本书就显的太单调了。书中的人物打仗完全不考虑粮草、地形、政治、士气、人心向背等因素。永远都是主将PK,各显法宝。看多了实在是视觉疲劳了。同样的套路太多了,让我忍不住想要跳过。最后我觉得虽然都是写神魔鬼怪,各种神仙,本书比西游记差的太多了。也难怪四大名著选了西游记。西游记的文笔比封神来说好太多了,封神最大的问题就是人物对话、战争描写、各种法宝都是千篇一律的场面话,看多了容易审美疲劳。作为古书能传到现在靠的是流传至今的故事:哪吒脑海、狐狸精迷惑纣王、纣王造炮烙虿盆、比干剜心、纣王欺贾氏(黄飞虎之妻)、黄飞虎闯五关、文王厄尔演周易、姜太公钓鱼等等。如果不是抱着系统性的梳理一下这些故事我可能不会看完。最后故事本身很精彩但是书确实一般,本书属于好的故事被讲烂了。
2026年09月19日
0 阅读
0 评论
0 点赞
2026-09-19
《elisp 每日一练》——abbrev table
一般,定义Major Mode 的时候需要管理 hook、mode-class、mode-name、keymap、abbrev-table、syntax-table这么六种资源。我们在前面介绍buffer 相关内容的时候我们跳过了 abbrev-table 的介绍,仅仅简单介绍了 syntax-table。在正式探索Major Mode 管理 abbrev-table 的秘密之前,需要先讨论一下 abbrev-table 的基础以及基本的使用方法基础知识所谓的 abbrev-table 它的全称是 Abbreviation Table 缩写表。读者如果对我之前写的vim script 教程有映像的话应该了解,abbrev 可以做到根据我们输入的简短的几个字母或者单词帮助我们扩展出想要的内容。例如我们输入 fn 按下 回车或者空格或者其他能表示输入关键字结束的按键,它会自动帮助我们将 fn 替换为 function一般来讲,一个 Abbrev Table 就是一个专门用来存储“缩写词 --> 展开词”映射关系的哈希表/查找表。与变量类似,Emacs中也有两种类型:buffer local 和 global 全局。buffer local 中的 abbrev-table 变量名称为 local-abbrev-table,而 全局的 abbrev-table 名称为 global-abbrev-table。定义并启用 abbrev-table我们可以通过 define-abbrev-table 定义一个缩写表。它的函数签名如下:(define-abbrev-table TABLENAME DEFINITIONS &optional DOCSTRING &rest PROPS)TABLENAME 是当前表的名称,它是一个符号DEFINITIONS 是一组定义好的映射关系,它是一个列表,列表的每一项都是一组 (key value) 形式的映射DOCSTRING 是当前列表的文档PROPS 是一个属性的列表。与 keymap 类似,它也支持继承机制,我们可以通过 PROPS 中的 :parents 定义父表。例如我们可以在 scratch buffer 中定义如下的 abbrev-table(define-abbrev-table 'my/list-abbrev-table '(("df" "(defun ()\n)") ("dm" "(defmacro ())")))我们可以通过开启buffer 的 abbrev-mode 来启用 abbrev-table,Emacs会根据输入的内容自动触发缩写,也可以调用 expand-abbrev 手动通知Emacs触发。在触发的时候Emacs会根据事先定义的 abbrev-table,按照优先查找 local-abbrev-table 然后查找 global-abbrev-table 的顺序。所以如果我们希望我们定义的 abbrev-table 能启用,可以使用 setq 将我们定义的变量绑定到 local-abbrev-table。因此我们如果要启用自定义的 abbrev-table,完整的代码应该如下:(define-abbrev-table 'my/list-abbrev-table '(("df" "(defun ()\n)") ("dm" "(defmacro ())"))) (setq local-abbrev-table my/list-abbrev-table) (abbrev-mode 1) ;; 也可以不启用,手动使用 expand-abbrev 主动触发abbrev table 的编辑Emacs 中并没有定义类似的 inert 或者 delete 这样的函数来修改某一项的映射关系,如果要修改某一项或者插入,都是通过 define-abbrev 来进行。删除操作我们只需要将对应项设置为 nil,而修改只需要对某项重新赋值即可。插入的话新定义一项原来没有的就可以了。例如对于lisp mode,我们加入快速定义交互式函数的项(define-abbrev my/list-abbrev-table "dfi" "(defun()\n(interactive))")如果想要一次性清空可以使用 clear-abbrev-table。Emacs还提供了一个交互式的修改方案,可以使用 edit-abbrevs 命令。该命令会打开一个新的窗口显示当前启用的 abbrev 项,我们可以直接进行修改,最后执行 C-c C-c 保存。持久化保存对于 abbrev-table 数据来说,我们并不会经常改动,可以说它适合作为外部的配置文件。Emacs当然有提供的手段。缩写默认定义在 ~/.emacs.d/abbrev_defs 中,我们可以修改 abbrev-file-name 变量来修改默认保存的位置。因此我们可以采用更合理的方式,例如:将 C/C++ 语言的数据保存在 ~/.emacs.d/abbrevs/c将 elisp 语言的数据保存在 ~/.emacs.d/abbrevs/elisp将 python 语言的数据保存在 ~/.emacs.d/abbrevs/py对于加载过来的数据,我们对于不合理的项可以使用上面提到的 define-abbrev 和 edit-abbrevs 进行编辑和修改,虽然在 edit-abbrevs 模式下,执行 C-c C-c 会默认执行 save-abbrevs 将内容保存到 abbrev-file-name 所指定的文件中,但是我们同样可以在关闭Emacs之前可以使用 write-abbrev-file 将当前buffer中的 abbrev 写入到指定文件保存。例如在scratch buffer中,我们使用 (write-abbrev-file (expand-file-name "~/.emacs.d/abbrevs/elisp")) 进行保存。对于保存的内容,我们可以通过 (quietly-read-abbrev-file &optional filename) 来读取。练习今天的练习我们就涉及Major Mode 了,我们根据上文使用 write-abbrev-file 保存的文件。编写一段加载指定 abbrev-file 并启用的代码(defun my/load-abbrev-file (filename) (interactive "fAbbrev File: ") (if (file-exists-p filename) (progn (quietly-read-abbrev-file filename) (setq local-abbrev-table my/list-abbrev-table) (abbrev-mode t))))上述代码非常简单。如果我们查看保存的内容会发现,它将我们通过 define-abbrev-table 的整个表达式都拷贝到对应的文件中,所以使用 quietly-read-abbrev-file 读取并执行代码之后会生成一个对应名称的变量,后面我只需要设置当前的 local-abbrev-table 为当前变量即可。总结本篇我们入门了 abbrev-table 的基础概念以及基本的操作API,本节涉及到的函数有:define-abbrev-tabledefine-abbrevwrite-abbrev-filequietly-read-abbrev-file
2026年09月19日
2 阅读
0 评论
0 点赞
2026-09-18
《elisp 每日一练》——Major Mode 中的keymap 管理
本节我们来介绍 Major Mode 中对于keymap 的管理,我们前面介绍过 define-minor-mode 中 :keymap 关键字。 但是我们查询文档的话发现,它并不支持 :keymap 关键字背景知识我们还是准备一个测试用例,看看宏展开是什么样的(defvar my-code-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c c c") #'compile) (define-key map (kbd "C-c c r") #'recompile) map) "Keymap for `my-code-mode'.") (define-derived-mode my-code-mode prog-mode "MyCode" "带有快捷键绑定的代码主模式。")在函数定义开始之前,它是这样处理keymap的(unless (boundp 'my/code-mode-map) (put 'my/code-mode-map 'definition-name 'my/code-mode)) (with-no-warnings (defvar my/code-mode-map (make-sparse-keymap))) (unless (get 'my/code-mode-map 'variable-documentation) (put 'my/code-mode-map 'variable-documentation (purecopy "Keymap for `my/code-mode'.")))这段代码表明,Major Mode 处理 keymap 的时候强制使用 mode 名称后带 -map 的符号。我们无法使用自己名称命名的keymap。(unless (boundp 'my/code-mode-map) (put 'my/code-mode-map 'definition-name 'my/code-mode))上述代码先检查对应名称的keymap 变量是否被绑定,如果没有被绑定,它会给这个符号挂一个 definition-name 属性,属性的值是mode 的符号。这个属性是 Emacs 29 引入的,用于告诉 find-function / describe-symbol 之类的工具:“这个 keymap 变量是由 my/code-mode 这个模式定义出来的”,方便用户跳转到定义处。后面它使用 defvar 直接重新定义了一个新的变量。请注意我们之前提到过 defvar 的特性,如果变量未定义则定义,如果变量已定义它就什么都不做。这里因为我们提前定义了同名的变量,所以这段代码实际上是什么都没有做。(unless (get 'my/code-mode-map 'variable-documentation) (put 'my/code-mode-map 'variable-documentation (purecopy "Keymap for `my/code-mode'.")))这段我们之前介绍过,它是在设置变量的文档,如果文档存在则不进行操作。函数中,keymap 采用的继承的方案,这部分的内容我们在之前已经详细分析过了,就不再继续了。总结本篇我们详细分析了Major Mode 中 keymap 的管理。与 minor mode 不同,我们发现它不支持通过 :keymap 关键字自定义 keymap 的名称,而是必须采用统一的名称:mode名+ -map
2026年09月18日
2 阅读
0 评论
0 点赞
2026-09-17
《elisp 每一一练》——Major Mode 继承机制
前面我们在介绍定义Major Mode 的宏 define-derived-mode 时提到,它本身是支持继承的,并且它有一个固定参数用于传入父Mode。今天我们来剖析一下它是如何实现继承以及继承哪些内容背景知识继承机制的分析我们还是一样的,使用简单的代码进行宏展开:(define-derived-mode my/code-mode prog-mode "MyCode" "继承自 prog-mode 的代码主模式")展开之后,我们观察一下,它主要发生了如下变化:(if (fboundp 'derived-mode-set-parent) (derived-mode-set-parent 'my/code-mode 'prog-mode) (put 'my/code-mode 'derived-mode-parent 'prog-mode))首先我们看到,这段代码通过 derived-mode-set-parent 为当前我们定义的mode 设置了一个父 mode。而之前的代码中调用 derived-mode-set-parent 设置的是 nil。derived-mode-set-parent 是Emacs 30.1 引入的新函数,用于声明Mode之前的继承关系。它的实现非常简单:(defun derived-mode-set-parent (mode parent) "Declare PARENT to be the parent of MODE." (put mode 'derived-mode-parent parent) (derived-mode--flush mode))我们看到它就干了两件事,一个是给mode的 'derived-mode-parent 属性设置成父mode,然后刷新相关的内部缓存,确保之后 Emacs 能正确处理继承关系。昨天我们简单的看了一下加载mode 的函数,在没有继承的情况下它比较简单,就是简单的设置了对应buffer local 变量的值为nil。加上继承之后,它的代码有了一些变化(defun my-code-mode nil "继承自 prog-mode 的代码主模式。\n\nIn addition to any hooks its parent mode `prog-mode' might have run,\nthis mode runs the hook `my-code-mode-hook', as the final or\npenultimate step during initialization.\n\n\\{my-code-mode-map}" (interactive) (delay-mode-hooks (prog-mode) (setq major-mode 'my-code-mode) (setq mode-name "MyCode") (progn (if (get 'prog-mode 'mode-class) (put 'my-code-mode 'mode-class (get 'prog-mode 'mode-class))) (unless (keymap-parent my-code-mode-map) (set-keymap-parent my-code-mode-map (current-local-map))) (let ((parent (char-table-parent my-code-mode-syntax-table))) (unless (and parent (not (eq parent (standard-syntax-table)))) (set-char-table-parent my-code-mode-syntax-table (syntax-table)))) (unless (or (abbrev-table-get my-code-mode-abbrev-table :parents) (eq my-code-mode-abbrev-table local-abbrev-table)) (abbrev-table-put my-code-mode-abbrev-table :parents (list local-abbrev-table)))) (use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)) (run-mode-hooks 'my-code-mode-hook))通过后面的代码,我们看到,实际上继承管理的是这么6个资源: keymap、syntax-table、abbrev-table、mode-class、major-mode 变量和 mode-name 变量。这些代码正好为这些资源建立了对应的父子关系。首先它调用 (prog-mode) 先初始化 prog-mode 的环境,熟悉面向对象编程的读者应该对这种写法不陌生,初始化子类之前先初始化父类算是面向对象的基本操作。然后通过 setq 来修正 major-mode 和 mode-name 变量。之后就是设置 mode-class 属性了(if (get 'prog-mode 'mode-class) (put 'my-code-mode 'mode-class (get 'prog-mode 'mode-class)))mode-class 本身是Emacs用于标记Sepcial Mode 的一个符号属性。一个Major Mode 如何被标记为 special,表示这个buffer中的内容是由某个mode自主生成的数据,不允许进行编辑。所以这段代码就很明显了,它获取 prog-mode 下的 mode-class 属性,如果有特殊标记,那么将它的值赋值给当前我们的mode接下来就是设置keymap 的继承关系了。(unless (keymap-parent my-code-mode-map) (set-keymap-parent my-code-mode-map (current-local-map)))与面向对象中的继承类似,子类可以重写父类的方法,对于 keymap 来说, 子 Mode 的快捷键可以覆盖父Mode。上述代码的含义就是如果我们定义的 mode 里面的 keymap 如果没有 parnet,那么就将 (current-local-map) 赋值给 parent 作为它的父类 keymap,因为我们之前调用 (prog-mode) 用父 mode 中变量初始化了当前 buffer,所以 (current-local-map) 实际取到的是 prog-mode 的 keymap。后门我们一次把 syntax-table和 abbrev-table 看完,它们都是相同的逻辑(let ((parent (char-table-parent my-code-mode-syntax-table))) (unless (and parent (not (eq parent (standard-syntax-table)))) (set-char-table-parent my-code-mode-syntax-table (syntax-table)))) (unless (or (abbrev-table-get my-code-mode-abbrev-table :parents) (eq my-code-mode-abbrev-table local-abbrev-table)) (abbrev-table-put my-code-mode-abbrev-table :parents (list local-abbrev-table)))与 syntax-table 不同的是, abbrev-table 是通过属性 :parent 来存储父Mode信息的。最后在这些资源都初始化完成之后,就是修改当前 buffer 中的 keymap 、syntax-table、abbrev-table 为子mode中的内容了。(use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)在初始化完了这些信息之后,正式通过上述代码由子mode中的资源接管原来 prog-mode 设置的buffer local 环境。常用的父类 mode一般来讲,我们开发一个新的major mode 并不会从0开始,而是选用合适的内置mode进行继承。一般来讲,常见的基础mode有:prog-mode 编程语言的基础mode,默认启用语法高亮、代码缩进引擎机制,绑定代码注释逻辑,排斥拼写检查等文本特征。text-mode 默认启用段落分隔规则、支持 flyspell-mode 拼写检查,代码高亮引擎默认留空special-mode 它是只读/交互式buffer 的基础,默认当前buffer 是只读的,并且绑定了 q 作为退出fundamental-mode 是Emacs最通用、无任何特殊定义的模式。因为所有的buffer都必须有一个mode,所以Emacs在无法确定当前buffer时候何种mode的时候就默认使用它上面我们提到,mode-class 是一个特殊的标记,读者可以尝试获取这些mode 的 mode-class 属性,看看它们具体是什么值。总结所以在存在继承关系的时候,它首先通过 derived-mode-set-parent 或者手动设置 mode 的 derived-mode-parent 属性的方式设置对应的继承关系,以便Emacs能正确处理它们。在加载mode 的函数中,它执行了下面的操作:启用父类的 mode ,用父类的"构造函数"初始化环境更新 major-mode 和 mode-name 变量更新 mode-class 标志,主要继承自父类设置 keymap 、syntax-table、abbrev-table 的父类,确定可以继承父类的内容更新当前buffer中 keymap 、syntax-table、abbrev-table 为子类的内容在编写自己的mode时通常使用内置的mode作为父类,Emacs中有一些父类提供了通用的模板: prog-mode 用于编程语言的mode、text-mode 用于文档类型的mode、special-mode 用于交互式内容
2026年09月17日
2 阅读
0 评论
0 点赞
2026-09-16
《elisp 每日一练》——Major Mode 初步探索
在前面的文章中,我们详细分析了关于 minor mode 对于各种资源的管理,最后以一个修改宏增加参数的例子结束了关于minor mode 的话题。今天我们正式进入有关 Major Mode 的话题背景知识与Minor Mode 不同,一个buffer 只能有一个Major Mode。我们可以使用宏 define-derived-mode 来定义 Major Mode。你可能会感觉很奇怪,为什么minor mode 的定义是使用 define-minor-mode 而 Major Mode 是不是使用 define-major-mode 来定义了。这是因为 Major Mode 引入了继承机制。(define-derived-mode CHILD PARENT NAME [DOCSTRING] [KEYWORD-ARGS...] &rest BODY)CHILD 新定义的Major Mode 的名称,与 define-minor-mode 的第一个参数相同PARENT 父类Major Mode 的名称NAME Major 的字符串名称KEYWORD-ARGS 可选的关键字参数对,用于精细控制模式的初始化行为我们还是用一个最简单的例子看看它展开之后是什么样子的(define-derived-mode my/foucs-mode nil "My Focus")展开之后,显示如下内容:(progn (defvar my/foucs-mode-hook nil) (unless (get 'my/foucs-mode-hook 'variable-documentation) (put 'my/foucs-mode-hook 'variable-documentation "Hook run after entering `my/foucs-mode'.\nNo problems result if this variable is not bound.\n`add-hook' automatically binds it. (This is true for all hook variables.)")) (unless (boundp 'my/foucs-mode-map) (put 'my/foucs-mode-map 'definition-name 'my/foucs-mode)) (with-no-warnings (defvar my/foucs-mode-map (make-sparse-keymap))) (unless (get 'my/foucs-mode-map 'variable-documentation) (put 'my/foucs-mode-map 'variable-documentation (purecopy "Keymap for `my/foucs-mode'."))) (progn (defvar my/foucs-mode-syntax-table) (unless (boundp 'my/foucs-mode-syntax-table) (put 'my/foucs-mode-syntax-table 'definition-name 'my/foucs-mode) (defvar my/foucs-mode-syntax-table (make-syntax-table))) (unless (get 'my/foucs-mode-syntax-table 'variable-documentation) (put 'my/foucs-mode-syntax-table 'variable-documentation (purecopy "Syntax table for `my/foucs-mode'.")))) (progn (defvar my/foucs-mode-abbrev-table) (unless (boundp 'my/foucs-mode-abbrev-table) (put 'my/foucs-mode-abbrev-table 'definition-name 'my/foucs-mode) (defvar my/foucs-mode-abbrev-table (progn (define-abbrev-table 'my/foucs-mode-abbrev-table nil) my/foucs-mode-abbrev-table))) (unless (get 'my/foucs-mode-abbrev-table 'variable-documentation) (put 'my/foucs-mode-abbrev-table 'variable-documentation (purecopy "Abbrev table for `my/foucs-mode'.")))) (if (fboundp 'derived-mode-set-parent) (derived-mode-set-parent 'my/foucs-mode 'nil) (put 'my/foucs-mode 'derived-mode-parent 'nil)) nil (defun my/foucs-mode nil "Major-mode.\nUses keymap `my/foucs-mode-map', abbrev table\n`my/foucs-mode-abbrev-table' and syntax-table\n`my/foucs-mode-syntax-table'.\n\nThis mode runs the hook `my/foucs-mode-hook', as the final or\npenultimate step during initialization.\n\n\\{my/foucs-mode-map}" (interactive) (delay-mode-hooks (kill-all-local-variables) (setq major-mode 'my/foucs-mode) (setq mode-name "My Focus") nil (use-local-map my/foucs-mode-map) (set-syntax-table my/foucs-mode-syntax-table) (setq local-abbrev-table my/foucs-mode-abbrev-table)) (run-mode-hooks 'my/foucs-mode-hook)))这段内容看起来比 define-minor-mode 生成的要简短一些。我们还是来看看它每一步都做了什么:首先定义并文档化了一系列的变量: hook、mode—keymap、syntax-table、abbrev-table通过 derived-mode-set-parent 记录mode间继承的父子关系定义了一个对应的 mode 函数请注意,这个函数与 minor mode 不同,它并不是一个切换的函数,也没有定义记录 mode 开启或者关闭的标志变量。这是因为Major Mode 本身在设计时就不支持随意的关闭或者开启。从设计上,每个buffer 必须有一个,但是最多只能有一个 Major Mode, Major Mode 是不支持随意开关的,如果想要替换Major Mode,它是强制清空上一个Major Mode 的所有设置并加载新的。所以每次调用这个mode函数实质上是强行占领了之前mode 的位置。有了这个前提,我们再来看看这个函数具体的操作。函数所有的代码都被包裹在 delay-mode-hooks 宏中,这个宏可以保证里面的所有代码在hook之前运行。通过 kill-all-local-variables 清理当前buffer的所有 buffer local 变量记录当前 major mode 到变量 major-mode 中设置变量 mode-name 为我们传入的mode 的use-local-map 来给mode安装快捷键映射set-syntax-table 和 local-abbrev-table 设置当前的 syntax table 和 abbrev table最后才运行对应的hook总结从这里我们看到,设计上 major mode 一个buffer 有且只有一个,所以在设计上就比较简单粗暴,它不考虑多个mode并存或者切换的场景,它简单的清理前任留下来的东西,直接加入自己的。所以它的代码比minor mode 要简单的多。但是它涉及到的概念可一点也不比 minor mode 少,除了hook、keymap 这些我们经常见到的内容,还有 syntax table、abbrev table和继承这类概念。这些我会在后面的文章中一一介绍
2026年09月16日
5 阅读
0 评论
0 点赞
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日
3 阅读
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日
1 阅读
0 评论
0 点赞
1
2
...
45