在前面的一系列篇幅中,我们详细讨论了 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统一加载使用
评论 (0)