《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 用于交互式内容

浙公网安备 33010602011771号