《elisp 每日一练》—— 处理复杂与多字符注释

《elisp 每日一练》—— 处理复杂与多字符注释

Masimaro
2026-09-23 / 0 评论 / 0 阅读 / 正在检测是否收录...

在上一节,我们提到使用 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++ 语言的角度来说,它是一个需要用户自己实现的接口。函数接收两个参数 startend, 表示需要处理的缓冲区范围。函数内部需要扫描这个区域,给区域内对应的字符设置 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扫描时传入的 startend 正好包含了完整的 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-function

syntax-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 完成。

0

评论 (0)

取消