首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
299 阅读
2
从零开始配置 vim(15)——状态栏配置
175 阅读
3
nvim番外之将配置的插件管理器更新为lazy
164 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
130 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
468
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
7
篇与
的结果
2026-10-13
《elisp 每日一练》——major mode 综合练习
截止到上一篇,我们完成了对major mode 相关概念的讨论,我们还是按照惯例先回顾一下前面的内容,接着来一个综合性的练习收尾知识回顾一个buffer 可以有多个 minor mode 但是有且只有一个major mode。我们可以通过 define-derived-mode 定义major mode。major mode 的定义具有继承的概念,一般是利用现在的major mode 扩展出新的。我们常用的基础major mode 有:fundamental-mode 最通用的major mode,Emacs新建buffer如果没有特殊指定,默认就是 fundamental-modetext-mode 一般的文本buffer都可以从它继承,它含有基础的单词、段落分割、拼写检查special-mode 它是只读交互buffer的基础, 绑定了 q 作为退出prog-mode 编程语言的基础mode、默认启用语法高亮、代码缩进引擎机制,绑定代码注释逻辑,排斥拼写检查等文本特征在定义我们的mode时,会管理 keymap、abbrev-table、syntax-table。它们都支持继承。继承方式各有不同:keymap 通过 set-keymap-parent 设置父类。abbrev-table 则使用 abbrev-table-put 为当前table 设置上 :parent 属性syntax-table 通过 set-char-table-parent 设置父类在设置完成之后,我们需要更新对应的buffer local 变量,更新的方式:(use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)这些工作都由宏来解决,我们的工作就是定义与 major mode 同名的 map、syntax-table、abbrev-table 变量。宏会自动帮我们完成设置继承关系、更新buffer local 变量这些工作。major mode 的一个重要工作是进行语法高亮,我们利用 font lock 和 syntax-table 配合进行也可以使用treesit 来完成这个工作。如果使用传统的 font lock,我们需要定义一个由正则表达式到对应face的映射关系,并且在最后将这个关系赋值到 font-lock-defaults 上如果使用 treesit, 我们采用查询语句的方式进行高亮。我们需要利用 treesit-font-lock-rules 将查询语句和对应face的映射关系进行编译,生成一个对应的list列表,后面我们需要将返回的列表设置到 buffer local 变量 treesit-font-lock-settings 上。并且需要设置 treesit-font-lock-feature-list 填入对应的feature 开启上面设置的某条高亮规则。最后需要调用 treesit-major-mode-setup 启动这套规则最后我们可以通过 auto-mode-alist 设置文件关联,这样下次在打开对应后缀的文件之后会直接使用我们定义的mode练习这部分的练习,我们来实现一个简单的支持 markdown 高亮显示的 mode。它的功能如下:对一些元素进行高亮显示:标题按照不同字号加粗显示,支持一些特殊的字符效果,例如加粗、斜体、删除线我们添加一个 cb 快速创建table、<c 快速创建代码块定义一个函数并绑定快捷键为 M-<Enter> 实现列表/标题智能追加。对于高亮,我们先定义基本的face(defface my-md-title-1 '((t :height 1.4 :weight bold :inherit font-lock-keyword-face)) "一级标题 Face(大字号+加粗)") (defface my-md-title-2 '((t :height 1.2 :weight bold :inherit font-lock-keyword-face)) "二级标题 Face (中字号+加粗)") (defface my-md-title-3 '((t :height 1.1 :weight bold :inherit font-lock-keyword-face)) "三级标题 Face(中小字号 + 加粗)") (defface my-md-title-4 '((t :weight bold :inherit font-lock-keyword-face)) "四级标题 Face(加粗)") (defface my-md-bold '((t :weight bold :inherit font-lock-type-face)) "粗体 Face") (defface my-md-italic '((t :slant italic :inherit font-lock-variable-name-face)) "斜体 Face") (defface my-md-strike '((t :strike-through "gray50" :inherit font-lock-comment-face)) "删除线 Face") ;; 针对这些face,模仿官方的 font-lock-face 变量定义 (defvar my-md-title-1 'my-md-title-1) (defvar my-md-title-2 'my-md-title-2) (defvar my-md-title-3 'my-md-title-3) (defvar my-md-title-4 'my-md-title-4) (defvar my-md-bold 'my-md-bold) (defvar my-md-italic 'my-md-italic) (defvar my-md-strike 'my-md-strike)然后我们针对markdown中这些标识通过正则表达式定义匹配规则(defvar my-md-font-lock-keywords `( ;; 一级标题 ("^#\\s-+\\(.*\\)$" 0 my-md-title-1) ;;二级标题 ("^##\\s-+\\(.*\\)$" 0 my-md-title-2) ;;三级标题 ("^###\\s-+\\(.*\\)$" 0 my-md-title-3) ;;四级及更高标题 ("^####+\\s-+\\(.*\\)$" 0 my-md-title-4) ;;粗体 ("\\*\\*\\([^*]+\\)\\*\\*" 1 my-md-bold) ;; 斜线 ("\\*\\([^*]+\\)\\*" 1 my-md-italic) ;;删除线 ("~~\\([^~]+\\)~~" 1 my-md-strike)) "My-Markdown 的 Font Lock 正则样式规则")然后我们定义一个简单 syntax-table,我们只识别字符串这一种词法块(defvar my-markdown-mode-syntax-table (let ((st (make-syntax-table))) (modify-syntax-entry ?\" "\"" st) st) "My Markdown Mode 语法表")接着我们定义一个 abbrev-table 添加两条规则(defvar my-markdown-mode-abbrev-table "My Markdown Mode 缩写表") (define-abbrev-table 'my-markdown-mode-abbrev-table '( ;;输入cel 自动展开代码块模板 (create elisp) ("cel" "```elisp\n\n```") ;;输出 tbl 创建一个表格的表头 ("tbl" "| Header 1 | Header 2|\n|:-----|:-----|\n| Item1 | Item2|")))对于自动追加对应的单元,我们通过正则表达式实现这么一个函数:(defun my-markdown-insert-sibling-line() "在当前行的下方插入同类型、同级别的行" (interactive) (let* ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))) (new-prefix (cond ;;无序列表, 以 - 、* 、+ 开头的行,前面可能带有缩进 ((string-match "^\\([ \t]*\\)\\([-*+]\\)\\([ \t]+\\)" line) (concat (match-string 1 line) (match-string 2 line) (match-string 3 line))) ;;有序列表 1. 或者 1) 等情形 ((string-match "^\\([ \t]*\\)\\([0-9]+\\)\\([.)]\\)\\([ \t]+\\)" line) (concat (match-string 1 line) (number-to-string (1+ (string-to-number (match-string 2 line)))) (match-string 3 line) (match-string 4 line))) ;; 标题 ((string-match "^\\(#+\\)[ \t]+" line) (concat (match-string 1 line) " ")) (t "")))) (end-of-line) (newline) (insert new-prefix)))我们实现了一个在这个函数,我们首先通过 buffer-substring-no-properties 获取当前行的文本,但是我们只需要文本而不关心它的属性。然后利用正则表达式进行匹配,分别尝试匹配各级标题、有序列表和无序列表。string-match 会将匹配结果存储,后续我们通过 match-string 按分组获取并最终合成一个对应的前缀。最后我们将光标移动到当前行的末尾并插入一行,然后在新行插入之前组织的前缀。最后我们对这个功能进行快捷键绑定(defvar my-markdown-mode-map (let ((map (make-sparse-keymap))) (keymap-set map "M-RET" #'my-markdown-insert-sibling-line) map))有了这些我们就可以组织成一个简单的 major mode(define-derived-mode my-markdown-mode text-mode "My Markdown" "简单的自定义 markdown major mode" (setq-default font-lock-defaults '(my-md-font-lock-keywords nil nil)) (abbrev-mode t) )根据前面我们分析的结果,只要 abbrev-table、syntax-table、keymap 符合默认的命名规则,define-derived-mode 会帮助我们处理这些,我们唯一需要额外设置的就是 font-lock 了。最后要启用缩写机制别忘了开启 abbrev-mode。我们可以用最后学到的 autoload 机制和文件绑定的方式,进行绑定。但是我这里就不再演示了。读者可以尝试使用它们使mode变得看起来更专业一些。总结到此为止,我们关于 minor mode 和 major mode 都结束了。但是我们在分析 define-derived-mode 和 define-minjor-mode 的时候忽略了hook。这部分我们将在接下来的hook专题中讨论。
2026年10月13日
4 阅读
0 评论
0 点赞
2026-10-12
《elisp 每日一练》——继承机制下 font lock 管理
在前面的一系列篇幅中,我们详细讨论了 font lock 机制,包括使用正则表达式和 treesit 进行语法高亮。那么在实际编写自己的Major Mode 时应该如何管理font lock 呢?本篇我们来讨论这个主题基础知识我们查阅 define-derived-mode 文档可以发现,它并没有提供类似 :font-lock 的关键字。而我们前面分析继承机制的时候,也没有关于 font lock 的继承机制。如果我们将前面几个篇章中 define-derived-mode 宏进行展开会发现,关于 font lock 的代码也就是我们在宏的 BODY 部分写下的那些内容。所以我们就能得到如下结论启用 font lock 需要为 buffer local 变量: font-lock-defaults 设置对应的映射关系。如果是treesit 则需要设置 buffer local 变量 treesit-font-lock-settings。后来的设置可以覆盖前面的设置,也就是我们定义子类的font lock 规则会完全覆盖父类的。define-derived-mode 本身是不管理 font lock 的。它的一切由用户代码进行处理。font lock 本身没有继承机制。define-derived-mode 也没有规定所谓的父子关系。我个人觉得,对于词法分析产生的 syntax-table、以及快捷键或者缩写确实可以有继承关系。但是编程语言这部分,实在很难抽象出一个通用的覆盖面广的 font lock 规则。不同的语言有不同的语法规则,关键字、运算符等等差距都比较大。所以最好的办法是将控制权完全交给具体的开发者。由开发者自由决定是在某个mode的基础上扩展还是重新打造。那么我们是否就没有办法从父类中完全继承呢?有些情况下利用已有的规则明显工作量比较少一些。例如C++ 之于 C、html 和 xml 这些明显有共同的方法。我们该如何做呢?常规的做法是在对应mode中,添加一个mode hook,在hook 中调用 font-lock-add-keywords 来追加专有的高亮规则。在 define-derived-mode 宏执行之后它会定义一个mode开启的函数在新的buffer 中执行这个mode函数,函数的第一句代码就是调用父类的mode启动函数,开始初始化父类的相关数据在父类的mode函数中,会使用 (setq-local font-lock-defaults ...) 来设置当前buffer的 font-lock-defaults 值。但是此时这个设置并没有形成真正的可以使用的映射规则在前面我们展示的展开代码中,mode函数的最后会执行 (run-mode-hooks 'my-code-mode-hook) 此时我们注册的mode hook函数会起作用。font-lock-add-keywords 的第一个参数是 mode,用来修改对应mode中的 font lock 规则。传入nil修改的就是当前mode中的,我们前面设置的 font-lock-defaults 变量。如果我们使用的是 treesit 的方式,那么情况就有点不同了。treesit 模式下,使用的是buffer local 变量 treesit-font-lock-settings。它是由函数 treesit-font-lock-rules 编译生成的一组着色规则。这一组规则本质上还是一个list。在设置之后需要主动调用 treesit-major-mode-setup 启用。所以针对这种情况的下的处理方式为:在调用父类的mode启动函数时,父类会设置 treesit-font-lock-settings 变量的值。在子类中,定义自己的规则使用list的操作函数追加到现在buffer 中的 treesit-font-lock-settings 列表中最后调用 treesit-major-mode-setup 启用我们来看看两种的方式不同的调用时机。使用传统的 font-lock-add-keywords 追加规则需要hook 中调用。而 treesit 方案是直接在定义新 mode 的宏里面 BODY部分定义。二者的不同其实是因为Emacs在初始化启用的时机不同导致的。对于 font-lock-add-keywords 方案,font-lock-add-keywords 函数并非单纯的修改一个匹配规则列表这么简单,它会触发 font-lock-set-defaults 进行状态检查。一旦发现当前buffer 的 font-lock-major-mode 与major mode 不同,它会重置当前buffer中的 font-lock-defaults 变量。而早期的Emacs中因为一个bug导致无法正确识别在子模式中识别出父模式,因此在宏body里面处理的话,它会认为仍然在父模式中,但是我们传入的却是子模式的符号,所以它会出现错误。而在mode hook中处理,它是子模式处理的最后一步,此时各种标志位已经设置完毕,它可以正确识别。而对于 treesit 来说,它没有类似的检测规则。它靠着 treesit-major-mode-setup 来启用设置的规则。在body中重新追加反而显得更加直观。另外因为hook执行在在mode初始化的最后,此时 treesit-major-mode-setup 早就执行了,此时在hook中反而显得多余。虽然理论上只要最后调用 treesit-major-mode-setup 就可以,但是多了一次调用,总会显的有些多余。总结今天我们纯粹是理论,我们来总结一下今天的主要内容:define-derived-mode 没有对 font lock 进行管理,完全依靠用户代码处理。想要继承,不同的高亮方案有不同的处理办法对于 font-lock-defaults 方案来说,最后需要在mode hook中调用 font-lock-add-keywords对于 treesit 方案来说,可以直接在宏的body中,定义新最规则,最后调用 append等list 操作函数追加。最终使用 treesit-major-mode-setup 统一加载使用
2026年10月12日
4 阅读
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日
5 阅读
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日
7 阅读
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日
7 阅读
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日
10 阅读
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日
9 阅读
0 评论
0 点赞