首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
305 阅读
2
从零开始配置 vim(15)——状态栏配置
178 阅读
3
nvim番外之将配置的插件管理器更新为lazy
165 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
133 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
472
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
6
篇与
的结果
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日
7 阅读
0 评论
0 点赞
2026-10-11
《elisp 每日一练》—— 基于treesitter 的语法高亮
在前面的一系列文章中,我们拆解了Emacs基于 syntax-table + 正则表达式 + font lock 的传统语法高亮的着色方案。虽然这套经典方案应对常规语言游刃有余,但一旦面对现代编程语言复杂的嵌套语法、泛型、模板字符串、异步闭包,单纯依靠上下文正则表达式就会力不从心。好在我们现在有基于 treesitter 语法解析树的新方案,而且Emacs从29开始也有了对treesitter 的支持。所以本节我们来讨论一下基于 treesitter 的现代语法高亮方式基础知识treesitter的基本使用我们在前面已经描述了。在这里我们再简单的回顾一下它的基本知识以及在Emacs中的使用方法。简单来讲, treesitter 是一个增量式底层解析器库。它在后台将你的代码实时编译成一棵精确的 AST。我们通过查询这颗树上的节点就能知道每个词法单元对应的语法单元。要在Emacs中使用它,我们首先要创建对应语言的解析引擎,我们可以使用 treesit-parser-create 来完成这一工作。在创建语法引擎成功之后,它会基于当前文本进行分析,得到具体的AST树。后续我们可以通过 treesit-buffer-root-node 获取树的根节点,然后我们可以使用下面的一些函数来遍历树节点并获取每个节点对应的语法单元:treesit-node-type 获取节点类型,例如 function_definition 或者 if_statement 等等treesit-node-text 获取节点对应的源码文本treesit-node-start 和 treesit-node-end 获取节点在缓冲区的起始或者结束位置treesit-node-child 获取节点的第N个子节点treesit-node-parent 获取节点的父节点treesit-node-prev-sibling 和 treesit-node-next-sibling 获取节点的前一个或者后一个兄弟节点treesit-node-check 检查节点的属性treesit-node-p 检查对象是否是一个 treesitter 节点treesit-node-eq 比较两个节点是否代表树中的同一个节点基于上述的内容,理论我们就能形成第一种方案,既遍历树上的每一个节点,根据节点属性来设置对应的face,完成高亮任务。但是考虑到遍历的方案有几个明显的缺点:对于需要频繁编辑的文件,例如正在做编码开发的部分,需要频繁的遍历整个文件,效率上差了很多Emacs并没有提供基于遍历的或者说基于node节点到font lock face 的映射,我们需要自己为对应的文本设置face属性。在需要更新的情况下更是如此除了遍历这种手段,treesitter 还能像数据库那样有一套自己的查询语句。我们可以根据查询语句来获取想要的部分。treesit-query-capture: 利用特定的语法,查找符合条件的接口, 可以接收list列表或者字符串treesit-query-string: 直接对字符串进行解析和查询treesit-query-validate: 验证查询语法是否正确客观来讲,使用查询的方案可能更加的直观,我们可以查询类似关键字、注释、字符串这样的语法单元,然后一次性的给所有这些单元挂上对应的face符号。针对查询语句的方案,Emacs提供了这样一个函数 treesit-font-lock-rule, 它的格式与我们前面介绍的类似,它里面传入 treesitter 查询语句到 face符号的映射。它会编译处理我们传入的这一组映射关系,最终返回一个可以供变量 treesit-font-lock-settings 使用的值。不仅来说这种方案更加直观、更易理解,而且对于频繁编辑的场景,Emacs对它做优化,它只进行增量更新,而不是全面的重新扫描。效率上也比遍历节点的方式更加高效。因此对于font lock着色的场景,查询语句的方案是首选。函数签名如下:(treesit-font-lock-rules &rest query-specs)它接收一系列的 query-spec 查询规范,每个规范由一个或多个 :keyword/value 对,以及紧随其后的一个 Tree-sitter 查询组成。关键字可以支持以下的类型::language: 语言的符号,例如对于c语言来说应该填写 'c,这个是必须填写的:feature : 功能符号,为查询设置一个功能名称,用户可通过 treesit-font-lock-level 等变量控制其启用/禁用, 这个也是必填项:override : 是否覆盖之前的显示效果,它的值可以有 nil、t、append、prepend、keep:default-language : 此关键字之后的所有查询默认使用该语言,除非被新的 :language 覆盖:reversed : 当该 feature 不在启用列表中时,反而启用此查询。在这些关键之后就需要跟上一个查询组了,查询组的每一项都由 查询语句 + 捕获组组成,捕获组是作用于查询语句的,我们可以直接使用 face 表示对查到的内容使用对应的样式进行着色,此时face前面需要加上一个 @。或者可以传入一个函数,函数的参数列表是 (node override start end),其中node 是匹配节点、override是我们在上面关键字中给出的 :override 的值,start 和 end 是对应文本在buffer中的范围。我们来看一个简单的例子:(treesit-font-lock-rules :language 'c :feature 'definition :override t ;; 查询模式 (Query Pattern) '((function_definition name: (identifier) @font-lock-function-name-face) (variable_declarator name: (identifier) @font-lock-variable-name-face))我们在前面简单介绍过 treesitter 的查询语句, 并且给出了查询语句的解决方案:新手看着 treesit-explore-mode 解析的结果来写条件。后续要启用这个条件需要记住将函数返回的值赋值到 treesit-font-lock-settings,并且设置 treesit-font-lock-feature-list 的值来开启对应feature标签下的匹配规则。练习我们今天的练习就是使用treesitter 来替换我们之前使用正则表达式来进行语法高亮的场景,并且在前面的基础上做一些新的语法高亮支持:以关键字的形式显示 if、else、while、for、return、break、continue、typedef、struct显示简单类型的语法高亮, int、float、double、char、void、bool,并且支持自定义的结构体和类类型高亮高亮显示常量, true、false、null、NULL显示注释的高亮(require 'treesit) (defvar my-ts-c-rules (treesit-font-lock-rules :default-language 'c ;; 注释 :feature 'comment '((comment) @font-lock-comment-face) ;; 字符串 :feature 'string '((string_literal) @font-lock-string-face) ;; 关键字 :feature 'keyword '((if_statement) @font-lock-keyword-face (return_statement) @font-lock-keyword-face (while_statement) @font-lock-keyword-face (for_statement) @font-lock-keyword-face (break_statement) @font-lock-keyword-face (continue_statement) @font-lock-keyword-face (struct_specifier) @font-lock-keyword-face) ;; 函数定义 / 变量 / 类型 :feature 'definition '((function_definition declarator: (function_declarator declarator: (identifier) @font-lock-function-name-face)) (init_declarator declarator: (identifier) @font-lock-variable-name-face) (type_identifier) @font-lock-type-face))) (define-derived-mode my-simple-ts-mode prog-mode "TS-C" (if (treesit-available-p) (progn (treesit-parser-create 'c) (setq-local treesit-font-lock-settings my-ts-c-rules) (setq-local treesit-font-lock-feature-list '((comment string keyword definition))) (treesit-major-mode-setup))))到此我们就完成了一个大概的高亮,但是在测试的过程中应该有其他的一些问题。读者可以参考Emacs内置的 c-ts-mode 来修改上面的匹配规则。总结今天我们大概回顾了一下treesit 的基本使用规则,并且使用 treesit 替换了之前使用正则表达式的语法高亮。想要启用treesit 的语法高亮最好使用查询语句的方式,我们可以通过 treesit-font-lock-rules 函数来设置匹配规则,它会根据参数返回一个可以供 treesit-font-lock-settings 使用的值。规则中 :language 和 :feature 十分重要是必填项,后续想要启用规则需要将规则对应的feature 标签传入到 treesit-font-lock-feature-list 中
2026年10月11日
8 阅读
0 评论
0 点赞
2026-10-10
《elisp 每日一练》—— syntax table 与 font lock
在前面一节的练习,我们做了一个简单的c 语言语法着色的效果。我们再回看一下代码发现,在设置关联的时候我们实际上没有设置 syntax-table,纯粹靠着正则表达式匹配特定字符和文本结构就完成了着色。这么做也有一个问题,就是它会遇到关键字、就着色,不管它出现的位置。读者可以使用下面的代码来测试一下// this is a line comment with keyword: return、this /*this is a block comment with type int、float*/ int main() { int x = 10; char* str = "this is a string with return and int"; return 0; }本篇我们的目的就是要搞清楚syntax-table 在语法着色的作用,以及我们如何利用syntax-table 来解决这一问题背景知识实际上font lock 着色主要依赖与下面三个类型:font-lock-keywords 里面的正则匹配,也就是我们前面定义的那个映射关系手动填充的 syntax-table 表,以及syntax-propertize-function 函数中将文本赋予的 syntax-table 属性font-lock-syntactic-face-function 函数决定的注释和字符串用什么类型的face我们说第一个是显性的,依赖完整正则匹配。后面两类却是依赖 syntax-table 的词法分析来完成工作。也就是说即使我们不使用 font lock 显式的通过正则表达式这种方式指定,只要我们正确的将对应的文本设置了syntax-table 属性,也可以完成语法着色的工作。正常情况下,我们首先根据编程语言的语法规则,为各个字符设置语法标签,填入到 syntax-table 中,例如将 /**/ 设置为注释,将 " 设置为字符串。接着在利用 font lock 进行语法高亮时调用 syntax-ppss 进行逐段的扫描(这个函数我们前面介绍过,它会按照语法表 syntax-table 自动跳转到下一个语法元素位置)。如果发现当前在字符串或者注释中就会使用 font-lock-comment-face / font-lock-string-face 给这段文本上色。这部分的工作都是通过 syntax-table 完成的对于块注释包含多行的情况下,Emacs 实际上还通过 syntax-propertize-function 利用文本属性中的 syntax-table 属性完成着色和 syntax-ppss 的跳转工作。最后因为 syntax-table 中只规定了那么几种词法样式,并没有规定类型、变量、函数、常量这种高级的语法特性,所以语法着色层面还是需要依赖我们前面定义的正则表达式匹配规则。最后我们再来看看我们之前设置的 font-lock-defaults 它到底接收什么样的值。(KEYWORDS [KEYWORDS-ONLY [CASE-FOLD [SYNTAX-ALIST ...]]])其中各个参数的含义如下:KEYWORDS: 就是我们前面一直在定义使用的规则,它可以是正则表达式和face 符号组成的cons cell,可以是正则表达式、捕获组和face符号组成的列表,也可以一个正则表达式带上多个捕获组和faceKEYWORDS-ONLY: 我们前面一直穿的是nil,nil表示即执行我们使用正则表达式在语法层面的着色,也执行 syntax-table 词法层面的着色,为 t 的话则只进行语法层面的着色CASE-FOLD 正则匹配时是否忽略大小写,为nil 时表示不忽略,t为忽略,此时像 int、Int、INT 这类的关键都会着色SYNTAX-ALIST 表示着色时使用的语法表,我们可以额外提供一个新的syntax-table 替换当前着色时使用的 syntax-table,默认使用的是当前buffer中的 syntax-table练习现在我们应该知道如何解决上面提到的,只要是 return、int 等关键字都会进行着色的问题了。我们实际上只需要添加关于注释和字符串的 syntax-table 项就可以解决这个问题(defvar my-simplec-mode-syntax-table (let ((st (make-syntax-table))) (modify-syntax-entry ?\" "\"" st) ;; 字符串 (modify-syntax-entry ?/ ". 124" st) ;; 注释 (modify-syntax-entry ?* ". 23" st) ;; 块注释 (modify-syntax-entry ?\n "> b" st) ;; 注释结束符 st)) (defvar my-simplec-font-lock-keywords `( ("\\<\\(if\\|else\\|while\\|for\\|return\\)\\>" . font-lock-keyword-face) ("\\<\\(int\\|float\\|double\\|char\\|void\\|bool\\)\\>" . font-lock-type-face) ("\\<\\(true\\|false\\|null\\|NULL\\)\\>" . font-lock-constant-face) ;; 函数定义 ("\\<\\([a-zA-Z0-9_]+\\)\\s-+\\([a-zA-Z0-9_]+\\)\\s-*(" (1 font-lock-type-face) (2 font-lock-function-name-face)) ;; 变量定义 ("\\<\\(int\\|float\\|double\\|char)\\s-+\\([a-zA-Z0-9_]+\\)" (1 font-lock-keyword-face) (2 font-lock-variable-name-face)) ) "MySimpleC 的 Font Lock 正则匹配与 Face 绑定规则。") (define-derived-mode my-simplec-mode text-mode "MySimpleC" ;; 这里为了避免 prog-mode 的干扰改成了 text-mode "配置简单的 Font Lock 关键字代码高亮" (setq-local font-lock-defaults '(my-simplec-font-lock-keywords nil nil)))这里我们添加了关于注释和字符串的 syntax-table 项,它们的着色就放到扫描时解决,因此就删除了正则表达式中关于注释着色的内容。总结到这里我们梳理了 syntax-table 在 font lock 中的作用。syntax-table 是我们在编辑时使用 forward-word 、syntax-ppss 等函数的基础。在此基础上,在启动 font lock 的时候会自动的利用syntab-table 进行词法层面的跳转,例如跳过注释。并且如果我们在 font-lock-defaults 参数中 KEYWORDS-ONLY 位置设置的是 nil 也会利用 syntax-table 中设置的字符串、注释等属性对这些元素进行着色。最后利用我们设置的正则表达式区分关键字、类型、变量、常量、函数名等语法单元进行语法层面的高亮。
2026年10月10日
5 阅读
0 评论
0 点赞
2026-10-09
《elisp 每日一练》——使用正则表达式捕获组实现上下文精准高亮
我们在上一节通过Emacs内置的font lock face 变量可以使用预定义的高亮显示face,如果需要修改高亮显示的颜色等可以考虑修改这些变量所在符号对应的face。但是我们上一节只实现了对于关键字这种类型的高亮,使用的是简单的正则表达式,主要匹配到对应的字符就高亮,相当于精确匹配。但是对于函数定义、变量定义、自定义类型这种高亮,前面的精确匹配就无能无力了,好在Emacs中我们可以使用正则表达式捕获组实现更加精确的匹配基础知识通过前面几节,我们发现定义语义与face高亮的映射关系实际上使用的是一个列表,每个列表组由匹配规则和对应的face符号组成。最简单的形式是一个cons cell(REGEXP . FACE)前面是一个正则表达式,后面跟一个face符号。我们在前面的练习中定义 while、for 等关键字时使用的是这种形式。实际上它还支持正则表达式捕获组。它真正的形式实际为(REGEXP SUBEXP FACE [OVERRIDE])此时它变成了一个列表,在正则表达式和face中间多了一个 SUBEXP 表示捕获组的整数。OVERRIDE 表示是否覆盖之前已经被匹配并进行高亮的部分,如果设置为t表示强制覆盖。如果一个正则表达式捕获的不同分组都是语法的一部分或者说要用不同的face显示,可以写为这样(REGEXP (SUBEXP1 FACE1) (SUBEXP2 FACE2))所以对于 int foo(...) 定义的函数来说,我们可以使用如下的高亮方案:'("\\<\\([a-zA-Z0-9_]+\\)\\s-+\\([a-zA-Z0-9_]+\\)\\s-*(" (1 font-lock-type-face) (2 font-lock-function-name-face))首先是前面由任意字符组成,与函数名之间通过空白字符分隔。所以对于第一个捕获组 [a-zA-Z0-9_] 来说,表示类型名。后面一个捕获组 ([a-zA-Z0-9_]+\\) 表示函数名称,它与函数参数列表的 ( 之间可以有或者没有没有分隔符。练习这次的练习我们在之前的基础上进行一个扩充,我们对简单的无参函数进行着色,并且对于使用简单数据类型int、float、double、char 等进行定义的变量进行着色。(defvar my-simplec-font-lock-keywords `( ("\\<\\(if\\|else\\|while\\|for\\|return\\)\\>" . font-lock-keyword-face) ("\\<\\(int\\|float\\|double\\|char\\|void\\|bool\\)\\>" . font-lock-type-face) ("\\<\\(true\\|false\\|null\\|NULL\\)\\>" . font-lock-constant-face) ("/\\*\\(?:.\\|\n\\)*?\\*/" 0 font-lock-comment-face) ("//.*$" 0 font-lock-comment-face) ;; 函数定义 ("\\<\\([a-zA-Z0-9_]+\\)\\s-+\\([a-zA-Z0-9_]+\\)\\s-*(" (1 font-lock-type-face) (2 font-lock-function-name-face)) ;; 变量定义 ("\\<\\(int\\|float\\|double\\|char)\\s-+\\([a-zA-Z0-9_]+\\)" (1 font-lock-keyword-face) (2 font-lock-variable-name-face)) ) "MySimpleC 的 Font Lock 正则匹配与 Face 绑定规则。") (define-derived-mode my-simplec-mode text-mode "MySimpleC" ;; 这里为了避免 prog-mode 的干扰改成了 text-mode "配置简单的 Font Lock 关键字代码高亮" (setq-local font-lock-defaults '(my-simplec-font-lock-keywords nil nil)))
2026年10月09日
6 阅读
0 评论
0 点赞
2026-10-08
《elisp 每日一练》—— 关键字高亮
在节前的一篇文章中,我们探讨了fontlock 的基本机制。它主要起到一个桥梁的作用,将语法块和face样式做一个绑定,未来在扫描时发现语法块符合匹配时就采用对应的样式显示文本。背景知识符号与 face 绑定在前面我们已经使用过face,我们知道要设置文本的显示样式,修改修改它的属性,而文本属性中有 face、font-lock-face、mouse-face 和 cursor 等等各种face。那么face 到底是什么?以及如何设置、修改一个face 呢?首先我在之前介绍Emacs基础语法的文章中提到,elisp中的符号这一概念。符号是一个容器,你可以将它想象成一个柜子,里面有若干个格子。每个格子可以存储不同的内容。对于elisp 来说,符号可以存储具体的值, 这种情况是它最像变量的一种情况了。(set 'num 10) num ;; ==> 10我们使用 setq 定义变量时执行的就是类似于 set 指令。setq 的全称是 set quote,它在一个符号存储值的格子中放入一个值。另一种情况,格子可以放下函数定义,这个时候我们通过(sym) 调用函数,实际上就是在查找 sym 符号中函数格子里面的放入的内容。它还可以放置一个 plist 属性列表,这个属性列表我们可以存储它的文档信息,也可以存储一个face表。我们可以通过 defface 定义一个face(defface v '((t :foreground "red")) "My face named v.")但是此时产生的效果与 setq 不同,setq 是创建一个符号,并在符号 value 槽中添加一个值。但是符号中并没有名为 face 的槽,defface 是在 外部的 face 映射表中添加了一条记录,由符号 v 作为键,而我们定义的这个face 样式表作为值。在后续查询的时候实际上是通过在表中查找对应符号名是否绑定了face条目。我们可以使用 face-attribute 来查询。通过 set-face-attribute 来修改。这里我们只需要记住face 本身并没有绑定到一个变量上或者符号上,我们将face绑定到具体符号上,并不会影响符号之前保存的内容。至于更详细的内容,读者可以自行查阅相关文档。Emacs 内置的 font-lock-face首先,我们在上一篇的例子中定义了名为 my-c-block-comment-face 的变量,作为注释的face。Emacs提供了一系列内置的准的 Font Lock Face。为了使我们开发的mode能轻松适配其他的theme,我们应该尽量使用Emacs自定的这些变量名。而如果我们要开发新的theme,也可以只修改这些变量的值。我们列举一些常用的:font-lock-keyword-face 关键字,例如 this、class、return 等等font-lock-type-face 数据类型或者类名font-lock-variable-name-face 变量名font-lock-function-name-face 函数名或者方法名font-lock-constant-face 常量或者字面量font-lock-builtin-face 语言内置函数或者指令font-lock-comment-face 注释以 font-lock-keyword-face 为例,我们来看看该如何使用它们。我们查看它的定义发现,它被定义在Emacs的 font-lock.el 文件中:(defvar font-lock-keyword-face 'font-lock-keyword-face "Face name to use for keywords.")我们发现这个变量被定义为了同名的符号。对它求值我们得到这个符号本身。对于c/c++ 我们可以这样定义关键字到具体face 的映射关系("\\<\\(if\\|else\\|while\\|for\\|return\\)\\>" . font-lock-keyword-face)这里我们定义了 if、else、while、for、return 关键字对应的face。因为使用Emacs定义了 font-lock-keyword-face 变量实际返回的是同名的符号,所以它实际上对应着我们在在上一篇文章中定义的规则:("/\\*\\(?:.\\|\n\\)*?\\*/" 0 'my-c-block-comment-face t)Emacs 在实际渲染是,查询到这些关键字都被映射到一个名为 font-lock-keyword-face 的符号上,它会查找外部face映射表中是否有对应符号的face。如果有则采用face中的定义来渲染。Emacs已经为它提供了默认的face(defface font-lock-keyword-face '((((class grayscale) (background light)) :foreground "LightGray" :weight bold) (((class grayscale) (background dark)) :foreground "DimGray" :weight bold) (((class color) (min-colors 88) (background light)) :foreground "Purple") (((class color) (min-colors 88) (background dark)) :foreground "Cyan1") (((class color) (min-colors 16) (background light)) :foreground "Purple") (((class color) (min-colors 16) (background dark)) :foreground "Cyan") (((class color) (min-colors 8)) :foreground "cyan" :weight bold) (t :weight bold)) "Font Lock mode face used to highlight keywords." :group 'font-lock-faces)所以我们如果想要修改显示效果,只需要通过 defface 来进行绑定,或者通过 set-face-attribute 设置。练习本次的练习,我们尝试通过上面介绍的变量定义一个简单的c语言的语法高亮:以关键字的形式显示 if、else、while、for、return、break、continue显示简单类型的语法高亮, int、float、double、char、void、bool高亮显示常量, true、false、null、NULL显示注释的高亮, 行注释和块注释,都是同一种样式(defvar my-simplec-font-lock-keywords `( ("\\<\\(if\\|else\\|while\\|for\\|return\\)\\>" . font-lock-keyword-face) ("\\<\\(int\\|float\\|double\\|char\\|void\\|bool\\)\\>" . font-lock-type-face) ("\\<\\(true\\|false\\|null\\|NULL\\)\\>" . font-lock-constant-face) ("/\\*\\(?:.\\|\n\\)*?\\*/" 0 font-lock-comment-face) ("//.*$" 0 font-lock-comment-face) ) "MySimpleC 的 Font Lock 正则匹配与 Face 绑定规则。") (define-derived-mode my-simplec-mode prog-mode "MySimpleC" "配置简单的 Font Lock 关键字代码高亮" (setq-local font-lock-defaults '(my-simplec-font-lock-keywords nil nil)))总结今天我们讨论了symbol 和 face 的绑定关系,symbol 本身可以绑定一个值、一个函数和一个plist 结构,但是无法绑定face,通过 defface 实际上是在外部建立了一个symbol 和 face 映射。在需要使用face时会查找这张表。另外Emacs中提供了一系列默认的 font lock face变量,它们的值是同名的 symbol,我们直接使用它来进行语法层面的关联。如果开发theme可以修改这些符号绑定的 face 即可
2026年10月08日
6 阅读
0 评论
0 点赞
2026-09-26
《elisp 每日一练》—— Font Lock 基础与语法高亮
各位读者中秋快乐,昨天中秋节,我去过了一天节,断更了一天,今天我们继续更新在前面的文章中,我们 将 Marjo Mode 中的六大元素都梳理了一遍。读者是否还记得哪六大元素呢?他们是 class-name、mode-name、keymap、abbrev-table、syntax-table、major-mode。有了这些元素我们就可以针对这些元素进行mode中的定制化开发。其中一项就是语法高亮,Emacs中的语法高亮依靠的是 Font Lock。背景知识Font Lock 本质是一个minor mode,负责根据文本的语法角色,自动为不同的语法角色加上 face属性,也就是文本的字体、字号、颜色、加粗或者斜体等样式。Font Lock 本身并不决定什么该高亮,它依靠Major Mode 为它标记各种语法类型,例如关键字、语句块、字符串、变量、函数等等。而Font Lock 作为中间的桥梁,将这些属性映射到具体的face上面,最终Emacs根据face属性进行高亮显示。这么说有点抽象,我们来看一个具体的示例, 它会将C/C++ 注释 /**/ 显示为黄色、// 注释显示为浅灰色。我们先来定义具体的语法表 syntax-table。对于这类注释的处理,我们在介绍 syntax-table 的时候已经介绍过了,现在直接拿来用即可(defvar my/c-like-mode-syntax-table (let ((table (make-syntax-table))) (modify-syntax-entry ?/ ". 124" table) ;; 注意 / 是两种注释都会使用的 (modify-syntax-entry ?* ". 23" table) (modify-syntax-entry ?\n "> b" table) ;; 这里使用style B 类型的注释 table))然后我们定义显示文本的face(defface my-c-block-comment-face '((t :foreground "yellow")) "Face for /*....*/ comments.") (defface my-c-line-comment-face '((t :foreground "light gray")) "Face for // comments")接着我们声明一个映射关系:(defvar my/c-like-mode-font-lock-keywords `(;; 规则 A:匹配 /* ... */ 块注释,最后加上 t 表示 override 覆盖默认注释 face ("/\\*\\(?:.\\|\n\\)*?\\*/" 0 'my-c-block-comment-face t) ;; 规则 B:匹配 // 单行注释,同样加上 t ("//.*$" 0 'my-c-line-comment-face t)) "测试模式的高亮规则列表。")最后我们定义buffer local 的 font-lock-default 变量,以便建立二者的映射关系。(define-derived-mode my/c-like-mode prog-mode "MySimpleC" (setq-local font-lock-defaults '(my/c-like-mode-font-lock-keywords)))我们来分析一下上述代码,深刻体会一下。首先我们定义一个语法表,与前面不同,我们添加了对于换行的处理,在 // 类型的注释中, 换行符是结束,而对于 /**/ 类型的注释,换行符只是其中的一部分。所以在定义换行符的时候,它只是作为 // 类型注释的结尾,这里我们使用了 style B 类型的注释。关于这部分内容读者有兴趣可以查找相关资料,这里表示的含义就是 换行符只作为 // 类型注释的结尾然后我们定义了 相关的face,来显示两种不同的注释。接着我们通过正则表达式来匹配,对于 /**/ 注释,我们采用定义的 my-c-block-comment-face 样式显示,对于 // 类型的注释,我们使用定义的样式 my-c-line-comment-face 来使用。总结本篇我们简单介绍了一下font lock 的相关机制,它是一个中间的桥梁,由Major Mode 定义语法规则,由用户的习惯定义不同语法块的显示样式,而font lock 负责作为桥梁将二者连接起来。这种机制为各种theme 主题提供了发挥的空间,用户可以通过更换主题来对不同的语法单元进行不同的着色,mode的作者可以专心处理各种语法规则,用户则不需要关注语法,可以选择自己喜欢的配色。同时也可以做到对于不同的语言,相同的语法单元显示相同的效果。对于本节我们展示的函数,我们将在接下来的文章中依依介绍。各位读者敬请期待!最后中秋国庆假期我要回家休假了,因此更新可能会等到国庆假期之后了。提前祝各位读者国庆快乐!!!!
2026年09月26日
12 阅读
1 评论
0 点赞