首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
302 阅读
2
从零开始配置 vim(15)——状态栏配置
176 阅读
3
nvim番外之将配置的插件管理器更新为lazy
165 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
132 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
471
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
471
篇与
的结果
2026-10-13
《elisp 每日一练》——文件关联与 Autoload 延迟加载机制
前面的一系列文章中, 我们差不多介绍完了 major mode 相关的一些概念。但是我们还有两个问题没有解决。之前的测试用例需要手动触发mode开启函数,我们需要的是它能根据一定的规则自动开启major mode。而且我们开发的major mode,我只希望在打开对应文件时加载,而不是无脑加载我们在 .emacs.d 中的所有配置。背景知识文件关联Emacs支持通过正则表达式对文件名进行匹配,根据匹配规则加载自动设置对应buffer的major mode。二者的匹配关系由全局的 auto-mode-alist 变量存储。列表中的每一项都是一个cons cell,它的写法为 (REGEX . mode-function) , 其中mode-function 是启动major mode的函数。例如我想要添加一条规则:所有的.c文件都使用我们之前开发的简易的 my-simplec-mode,可以写下如下代码:(add-to-list 'auto-mode-alist '("\\.c\\'" . my-simplec-mode)))autoload 延迟加载一般情况下,外部要引用子模块的内容,可以使用 require 关键字,假设我们将之前编写的 my-simplec-mode 放到一个名为 my-simplec-c.el 文件中,如果外部要加载可以写下:(require 'my-simplec-c)Emacs 在读到这句时会将整个文件都加载进来,并且对文件中所有代码执行。虽然它能保证文件中的代码执行一次,但是有时候文件中某些耗时的操作我并不会在开启时就执行,甚至有时候直到关闭Emacs都不会被执行。此时这种全盘执行的方案就有点浪费时间了。Emacs 允许我们对一些函数或者代码添加autoload 注释,以便延迟加载。注释的写法如下:;;;###autoload当我们需要执行一个 elisp 文件时,例如使用 load-file、package-install 时,它会扫描该文件,对于使用该注释修饰的函数,它不会执行而是生成一条记录,写入对应的 -autoloads.el 文件中,例如对于上面的 my-simplec-c.el ,它会生成一个 my-simplec-c-autoloads.el 的文件。文件中记录需要延迟加载的函数信息。在真正需要执行的时候再根据 autoloads 文件中记录的信息查找到具体代码位置,正式加载并执行函数。其实它的加载时机并不只有主动load的时候。而且我们前面说的启动Emacs时执行也不太准确。至于具体的时机我们将在后面详细讨论,在这一阶段读者只用记住autoload 是只在必须调用的时候加载,能加速启动即可。总结本篇我们简单讨论了一下关于 auto-mode-alist 和 autoload 的内容。前者可以将文件后缀和对应的mode绑定,后者可以做到延迟加载。要达到效果很简单,但是背后的原理我们在这里展开又太大了,后面我打算在打包与模块化等专题的时候详细讨论。
2026年10月13日
6 阅读
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日
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 点赞
2026-09-24
《elisp 每日一练》—— define-derived-mode 管理 syntax-table
在前面的文章中,我们了解了,实际在通过 define-derived-mode 定义自己的Major Mode 的时候,在处理 syntax-table 时主要通过继承的方式继承父类中内容,然后通过 modify-syntax-entry 修改不符合当前语境下的项目。或者使用 syntax-propertize-function 通过正则表达式处理一些复杂的情况。今天我们深入探讨 define-derived-mode 宏是如何管理 syntax-table 以及如何通过继承机制获得父类的 syntax-table背景知识syntax-table 关键字与 abbrev-table 类似,define-derived-mode 支持通过 :syntax-table 自定义syntax-table。我们通过一个简单的示例看看宏展开的代码:(define-derived-mode my/code-mode prog-mode "mycode" "my code 测试mode" :syntax-table (let ((my-code-syntax-table (make-syntax-table)))))我们展开之后发现,与 syntax-table 相关的代码只有(set-syntax-table (let ((my-code-syntax-table (make-syntax-table)))))也就是说它仅仅将我们通过 :syntax-table 关键字传进来的 内容设置为当前buffer中的 syntax-table。此时继承相关的内容由我们自己管理。因此这里得出的结论也是与 abbrev-table 类似:应该在外部定义一个与宏里面 syntax-table 命名相同的 syntax-table 变量,我们在外部只关心具体的定义,而其余细节交给宏来处理。set-char-table-parent我们观察在继承机制下,对于 syntax-table 的处理发现,继承机制实际上是通过 set-char-table-parent 函数来完成。(set-char-table-parent CHAR-TABLE PARENT)这个代码是底层通过 c 实现的,我们可以简单的看看在Emacs中它是如何实现的。在我找到的一些源码资料中显示,这个函数的C语言实现如下:DEFUN ("set-char-table-parent", Fset_char_table_parent, Sset_char_table_parent, 2, 2, 0, doc: /* Set the parent char-table of CHAR-TABLE to PARENT. Return PARENT. PARENT must be either nil or another char-table. */) (Lisp_Object char_table, Lisp_Object parent) { Lisp_Object temp; CHECK_CHAR_TABLE (char_table); if (!NILP (parent)) { CHECK_CHAR_TABLE (parent); for (temp = parent; !NILP (temp); temp = XCHAR_TABLE (temp)->parent) if (EQ (temp, char_table)) error ("Attempt to make a chartable be its own parent"); } set_char_table_parent (char_table, parent); return parent; } static void set_char_table_parent (Lisp_Object table, Lisp_Object val) { XCHAR_TABLE (table)->parent = val; }从这里我们可以看到,函数内部主要的工作实际上是将表中的 parent 指针指向真实的父类。所以说它的继承结构实际上构成了一个链表。按照我们写程序的一般规律来讲,如果是一个链表结构,可以合理的推测它的查找流程应该是这样的:先在当前buffer 中记录的 syntax-table 表中查找如果未找到,则根据 parent 指针找到它的父表,在父表中查找根据记录的 parent 指针一级级的往后找,直到找到或者 parent 指针是空我们可以在Emacs的源码中找到查表相关的代码:Lisp_Object char_table_ref (Lisp_Object table, int c) { struct Lisp_Char_Table *tbl = XCHAR_TABLE (table); Lisp_Object val; if (ASCII_CHAR_P (c)) { val = tbl->ascii; if (SUB_CHAR_TABLE_P (val)) val = XSUB_CHAR_TABLE (val)->contents[c]; } else { val = tbl->contents[CHARTAB_IDX (c, 0, 0)]; if (SUB_CHAR_TABLE_P (val)) val = sub_char_table_ref (val, c, UNIPROP_TABLE_P (table)); } if (NILP (val)) { val = tbl->defalt; if (NILP (val) && CHAR_TABLE_P (tbl->parent)) val = char_table_ref (tbl->parent, c); } return val; }别的先不用太纠结,我们看到它分了三种情况:如果是普通的 ASCII 字符,直接查找表中的 contents 数据如果是非 ASCII 字符,调用 sub_char_table_ref 查找最后如果当前表中未找到,它会通过 parent 指针找到父表,再递归调用一次上面的代码中,我们看不到 syntax-table 的相关名称,转而退化为了 char-table。实际上 char-table 是Emacs专门为了快速查找 key 类型为 unicode 字符,而value 类型为对应数据这种映射关系而设计的数据结构。syntax-table 只是它里面的一种。如果深入了解Emacs之后会发现很多类似的结构。总结最后我们来总结一下关于 syntax-table 的相关内容。默认情况下,define-derived-mode 会根据传入的 parent 参数来执行继承操作。实现继承的主要方式就是调用 set-char-table-parent 将多个表组成一个链表,实际查找时按链表顺序进行查找。我们可以使用 :syntax-table 关键字指定 syntax-table,但是这个时候需要我们自己处理关于表的一切,define-derived-mode 只负责设置当前buffer 的 syntax-table 为我们指定的内容。
2026年09月24日
7 阅读
0 评论
0 点赞
2026-09-23
《elisp 每日一练》—— 处理复杂与多字符注释
在上一节,我们提到使用 modify-syntax-entry 定义各种符号范围。对于注释来说,它支持通过简单的1到4 这样的数字标识符来表达注释范围,但是对于像xml中的 <!----> 和 <CDATA[]> 这样的结构就无能无力了。今天我们来看看如何处理这种复杂的情况。背景知识syntax-propertize-function 的基本使用对于xml中的注释,或者像python中支持的 """ 注释我们需要使用 syntax-propertize-function 函数来处理。昨天我们提到 syntax-propertize-function 时说它是一个函数实际上是不准确的,实际上它是 Emacs 语法解析的“动态补丁”机制。当 syntax-table 静态规则不够用的时候,它允许你根据缓冲区中的实际上下文为特定的临时打上 syntax-table 文本属性,从而覆盖原有的语法定义。我们打一个不那么恰当的例子,每个人的角色是多变的,他可以是一个父亲,一个丈夫,一名企业职工,也可以是一个儿子。同样的一个字符在不同位置表现出来的含义可能不同。前面我们说到 / 可以是除法运算符,或者作为注释开始的字符。而 # 在c语言中同样有不同的含义,在一段代码的开始位置可能是预处理指令,而在宏定义的中间,用来将参数转换为一个字符串字面量。所以说我们只会通过 modify-syntax-entry 设置字符的静态类型是不够的。syntax-propertize-function 实际是一个变量。变量应该被设置成一个函数,Emacs 在需要解析语法时会自动调用这个函数。如果从C/C++ 语言的角度来说,它是一个需要用户自己实现的接口。函数接收两个参数 start 和 end, 表示需要处理的缓冲区范围。函数内部需要扫描这个区域,给区域内对应的字符设置 syntax class 标识。与 syntax-table 相似的,我们可以定义一个以 mode 名称加 -syntax-propertize 命名的函数,然后通过 setq-local 设置当前buffer 的 syntax-propertize-function 为我们定义的函数。我们看一个具体的示例,C++ 11 新增了一种 R"()" 表达的字符串,它里面可以包含,""、\ ,这些都被视为字符串的一部分,再也不用频繁的使用 \ 对特殊字符进行转义了。针对这种情况,我们无法再简单的将 "" 中包裹的内容统一视为字符串了。我们需要判断"" 是否出现在 R"()" 中。我们可以写下如下代码:(defun my-raw-string-propertize (start end) "把 R\"(...)\" 标记为字符串。" (goto-char start) ;; 搜索开启符 R"( (while (re-search-forward "R\"(" end t) (let ((open-pos (match-beginning 0))) ; R"( 的起点 ;; 如果已经在字符串/注释里,跳过 (unless (nth 3 (syntax-ppss open-pos)) (save-excursion ;; 向后找结束符 )" (when (search-forward ")\"" nil t) (let ((close-pos (match-end 0))) ; )" 的终点 ;; 第一步:整段标成字符串 (put-text-property open-pos close-pos 'syntax-table (string-to-syntax "|")) ;; 第二步:把内容里的 " 覆盖成标点 (save-excursion (goto-char open-pos) (search-forward "(" nil t) ; 跳过 R"( (while (search-forward "\"" close-pos t) (put-text-property (1- (point)) (point) 'syntax-table (string-to-syntax ".")))))))))))上面的内容非常简单,我们先匹配 R"( 然后从这个位置往后匹配它的结束位置 )",将中间的部分标记为字符串。另外我们还取消了字符串中之前对于某些字符的定义。syntax-propertize-extend-region-functions hook 基本简介但是读者应该注意到,这段代码是有问题的。它基于一个假设,Emacs扫描时传入的 start 和 end 正好包含了完整的 R"()" 字符串表达。如果字符串比较长,而Emacs一次没有完全扫描完,可能出现这次无法找到结束标记,而下一次无法找到开始标记的问题。所以读者不用拿这段代码进行测试,我们只需要理解这个基本的思路即可。我们虽然可以通过 (point-min) 和 (point-max) 函数主动的扩大扫描的范围,但是要考虑到最终的性能。而且我们改了之后,Emacs并不知道,下一次问题仍然存在,在函数内部就又要执行一次,最终拖累整体性能。所以最好的办法就是我们在当前基础之上逐步向外寻找R"()" 的开始和结束标志,处理之后通知Emacs,更新范围。Emacs提供了 syntax-propertize-extend-region-functions 类型的hook函数,它在 syntax-propertize-function 被调用之前调用,这个hook函数返回一个新范围 (new-start . new-end) ,Emacs 下一次会按照这个范围再调用 syntax-propertize-function。所以整体的流程应该是:Emacs 调用 syntax-propertize-function 之前先调用 syntax-propertize-extend-region-functions。传入一个范围,假设范围是 start = 100 end = 300在函数 syntax-propertize-extend-region-functions 发现 R"()" 的范围可能跟想象中不一样,假设是 start = 50 end = 240,此时 syntax-propertize-extend-region-functions 会返回这个范围此时Emacs使用hook 函数返回的实际位置范围调用 syntax-propertize-functionsyntax-propertize-rules 宏实际上在 syntax-propertize-function 函数中的操作非常固定:使用正则表达式匹配一个范围,将范围内文本 syntax-table 属性进行修改。那么这种固定的工作可以抽象为一个函数或者宏,Emacs中提供了 syntax-propertize-rules 宏来完成这类工作。(syntax-propertize-rules &rest RULES)RULES 是一组规则,每组规则都由两个部分组成:正则表达和语法类型。例如对于xml 的注释来说,我们可以写下如下的匹配规则(setq-local syntax-propertize-function (syntax-propertize-rules ;; 当正则匹配到 <!-- 时,将最后一个 '-' 动态标记为“注释开始”('<') ("<!--" (0 "<")) ;; 当正则匹配到 --> 时,将第一个 '-' 动态标记为“注释结束”('>') ("-->" (0 ">"))))后面的 (0 "<") 表示正则表达式的捕获组。syntax-table 属性前面我们介绍了 modify-syntax-entry 它修改了语法表 syntax-table, emacs 利用它来实现快速跳转到下一个语法单元、高亮语法单元等等操作,可以说是文本进行词法分析的产物。但是文中给出的 syntax-propertize-function 函数操作,实际在修改文本的 syntax-table 属性,它们二者有什么共同点和不同呢?实际上buffer 的 syntax-table 和 文本属性的 syntax-table 是两套并存、但优先级不同的机制。它们最终都影响 Emacs 对“某个字符是什么语法类”的判断,但作用方式不同。Emacs在确定字符最终的语法属性时,优先查看字符上面是否带了 syntax-table 属性,如果有则采用属性记录的语法类型。如果没有则查找buffer 中的 syntax-table 。实际上我们已经可以意识到,和人的属性一样,一个字符在不同的位置有不同的语法属性,不可能永远是一种角色。单单依靠 syntax-table 语法表是无法处理复杂的情况的。所以Emacs在其基础上打了一个补丁。总结今天我们学习了通过 syntax-propertize-function 处理复杂的情况。该函数由Emacs调用,Emacs每次对一个区域进行扫描都会调用该函数。在函数中我们可以通过正则表达式或者其他方式设置文本的 syntax-table 属性,确定文本的语法类型。如果一次扫描无法覆盖整个语法区域,可以实现 hook 函数 syntax-propertize-extend-region-functions 调整扫描范围。而对于正则表达式匹配为文本赋值 syntax-table 属性,这种重复的工作可以交给 syntax-propertize-function 完成。
2026年09月23日
7 阅读
0 评论
0 点赞
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日
11 阅读
0 评论
0 点赞
1
2
3
4
...
48