《elisp 每日一练》——syntax-table

《elisp 每日一练》——syntax-table

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

前面我们都是将buffer中的文本作为一个普通的字符串进行处理,讨论的是普通字符串的固有操作方式。但是我们还有另一类特殊文本,编程语言和配置文件的文本。那么在这个领域Emacs又该如何处理它们呢?

背景知识

对于编程语言或者通用配置文件来说,如果使用传统的正则表达式不但难以理解,而且容易出错,后续的维护成本一定是巨大的。针对这种情况,Emacs通过为不同类型的文件配置了不同的 Major Mode 来支持类似语法解析、高亮还有一些高级的功能。

Emacs 中每一个Major Mode 都有一个自己的 Syntax Table,它是一个巨大的 char table。它你可以把它看作一个巨大的查找表。以字符编码为索引,通过查询这个表 Emacs 就能知道如何处理每个字符。我们可以使用 describe-variable 函数搜索 syntax-table。我们可以看到Emacs中有非常多的 syntax-table。例如对于编程语言来说有 prog-mode-syntax-tableemacs-lisp-mode-syntax-table。对于文本有 text-mode-syntax-table

通常在不同的mode下,syntax-table 的内容不同,所以在不同的编程语言中,同一个符号的含义可能不同,例如C/C++ 等一些语言中,; 表示语句结束,而 elisp 中表示注释。

一般来讲,我们不直接操作这个表。而是通过它提供的配套函数来完成这项工作。

首先要介绍的就是 forward-sexpbackward-sexpscan-sexps

forward-sexpbackward-sexp 是向前或者向后移动一个平衡表达式。它们接收一个 ARG 数字类型的参数。表示向前或者向后移动多少个单元,而这个数可以是负数,表示反向移动。

scan-sexps 则是它们背后的函数。它不移动点,而是计算并返回一个位置。它的定义如下:

(scan-sexps FROM COUNT)

它从 FROM 位置开始,向前(COUNT为正)或者向后(COUNT 为负)扫描 COUNT 个平衡表达式。然后返回扫描停止时的位置。

我们可以实现一个简单的标记当前光标所在语法单元的小例子看看怎么使用

(defun my/mark-sexp()
  (interactive)
  (let* ((cur (point))
     (start (scan-sexps cur -1))
     (end (scan-sexps start 1)))
    (push-mark start)
    (goto-char start)
    (push-mark end t t)))

上述代码比较简单,单纯的通过 scan-sexps 传入不同的参数让它从当前位置往前或者往后进行扫描以确定对应语法单元的起始位置。然后利用mark 和 当前光标位置组成的一个region来实现选中。

在实际进行词汇分析更常用的函数可能是 syntax-ppss ,它主要用来扫描从当前位置到指定位置范围内的文本,并确定其上下文。它底层调用 parse-partial-sexp ,但是比 parse-partial-sexp 多了增加了内部缓存,使得重复调用变得高效且开销极低。一般在开发Emacs lisp相关程序时会推荐使用 syntax-ppss 而避免使用 parse-partial-sexp

它返回一个包含十个元素的列表用来表示当前的状态,按顺序依次为:

  • 括号的嵌套深度
  • 最内层包含括号表达式的起始字符位置
  • 解析的最后一个完整表达式(S-表达式)起始处的字符位置
  • 是否在字符串内?如果在字符串内,则返回字符串终止符字符(例如 ?\" );对于通用字符串,则返回 t
  • 是否在注释内?如果在注释内则返回 t ,或返回特殊注释类型的整数表示
  • 如果点恰好位于转义下一个字符的引号字符之后,则返回 t
  • 本次扫描期间达到的最小括号深度。
  • 注释的样式
  • 当前字符串或注释起始处的字符位置
  • 当前未闭合括号位置的内部列表

练习

之前的练习,我们通过正则表达式来匹配注释中的todo或者fixme 信息,并加入到minibuffer 中,我们今天改一个写法,采用rx表达式找到todo、fixme 这样的字眼,然后通过 syntax-ppss 来判断是否处于注释中。

(defvar my/todo-mark-list '()
  "保存全局的 todo 列表,格式为 (SUMMARY-TEXT . marker)")

(defun my/code-todo-list-collect ()
  "收集当前 buffer 中位于注释里的 TODO/FIXME。"
  (interactive)
  (setq my/todo-mark-list '())
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (let ((todo-regexp (rx (or "todo" "fixme" "TODO" "FIXME"))))
        (while (re-search-forward todo-regexp nil t)
          (let ((beg (match-beginning 0)))
            ;; 只保留位于注释中的匹配
            (when (nth 4 (syntax-ppss beg)) ;; re-search-forward 在找到之后跳转光标到匹配位置的尾部,这里我们扫描尾部到开头位置范围内的词是否在注释内
              (let* ((raw-content (match-string 0))
                     (m (copy-marker beg)))
                (push (cons raw-content m) my/todo-mark-list)))))))))

其他位置的代码我就不放了,读者可以尝试一下在之前练习的基础上进行简单修改即可

总结

本篇我们介绍了Emacs中原始的 Syntax-Table 以及一些词法扫描的API函数。它们是

  • forward-sexpbackward-sexp
  • scan-sexps
  • syntax-ppss
0

评论 (0)

取消