《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,完成高亮任务。但是考虑到遍历的方案有几个明显的缺点:

  1. 对于需要频繁编辑的文件,例如正在做编码开发的部分,需要频繁的遍历整个文件,效率上差了很多
  2. 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着色的场景,查询语句的方案是首选。

函数签名如下:

emacs-lisp 复制代码
(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中的范围。我们来看一个简单的例子:

emacs-lisp 复制代码
(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
  • 显示注释的高亮
emacs-lisp 复制代码
(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 中

相关推荐
智购科技自动售货机工厂12 天前
数字人民币硬钱包支付失败,排查发现是NFC读卡器功率不足~YH
python·面试·架构·eclipse·emacs
Masimaro13 天前
《elisp 每日一练》—— define-derived-mode 管理 syntax-table
emacs
Masimaro21 天前
《elisp 每日一练》—— Major Mode 管理 abbrev-table
emacs
AI情绪识别开源23 天前
检信AI学业生涯智选系统技术方案概述
emacs
Masimaro1 个月前
《elisp 每日一练》—— Minor Mode 回顾与综合练习
emacs
aluluka1 个月前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs·lisp·elisp
aluluka1 个月前
《elisp 每日一练》——minor mode 快捷键管理
emacs·lisp·elisp
Masimaro1 个月前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs
Masimaro1 个月前
《elisp 每日一练》——minor mode 开关变量的管理
emacs