前面一篇文章中,我们提到 Syntax-Table。它相当于一张巨大的表,通过字符串索引到它的属性信息。但是它仅仅停留在词法层面,例如下面的一段c代码
int main(){return 0;}它仅仅能识别它们是 单词 单词 () {单词 数字 分号} 也就是给每个语法单元大了一个 Token,进行了简单的词法分析。但是它没有办法将这段代码解析成有意义的语法单元。导致我们没办法利用它进行高级的语法单元的解构。要实现类似IDE那种跳转或者语法高亮,需要借助第三方库,或者自己根据词法单元写一套语法分析器。
但是从Emacs 29开始,引入了 tree-sitter,针对所有编程语言提供了统一的接口,让我们能基于语法形成的AST,做一些高级的功能
背景知识
Emacs中使用 tree-sitter 围绕着 解析器 、语法节点 和 查询 展开的。这么说有些抽象,我们换一种说法就是调用函数解析当前buffer中的内容形成抽象语法树,遍历树节点获得每个节点中的属性,例如它是一个function或者是一个变量等等。最后对于一些提取数据的功能我们不需要遍历整个树,而是可以采用类似SQL的查询语句来找到想要的内容。
解析器
在执行语法分析前,我们需要检查环境以及定义使用的解析器。这方面的函数有:
treesit-available-p: 检查当前Emacs是否支持 tree-sittertreesit-language-available-p: 检查特定语言的语法定义是否已安装treesit-parser-create: 为当前缓冲区创建一个新的解析器treesit-parser-list: 获取当前缓冲区中的解析器列表treesit-parser-buffer: 获取解析器关联的缓冲区treesit-parser-language: 获取解析器所使用的语言
例如我们使用 (treesit-language-available-p 'elisp) 来判断当前elisp的解析器是否安装,对于一个裸Emacs来说,它会返回nil。
我们可以手动的通过 treesit-install-language-grammar 命令安装对应的语法解释器。或者我希望我的elisp配置帮我安装我需要的解析器这样即使我换了一台新的电脑,只要执行这些配置它也能帮我准备对应的环境。
我们可以先往列表 treesit-language-source-alist 中添加成员。它的每一项都是一个 (LANG . (URL REVISION SOURCE-DIR CC C++)) 结构,默认只需要填写LANG 和 URL,它们分别是对应的语言和解析器的github地址。后面几个参数: REVISION 表示要下载哪个版本,SOURCE-DIR 下载的源码在哪个目录、C 和 C++ 表示编译时使用的C/C++编译器。针对elisp我们可以这样指定
(add-to-list 'treesit-language-source-alist)之后我们可以通过下面的代码来一次安装 treesit-language-source-alist 中定义的所有的语言解析器
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))如果你使用的操作系统是Windows,因为Windows没有原生的gcc,我们可以使用MinGW-w64。如果你不想折腾这些,可以直接从GitHub上找到一些预编译项目例如 casouri/tree-sitter-module ,下载已经编译好的Windows版本,将它们拷贝到 ~/.emacs.d/tree-sitter/ 目录中。因为tree-sitter 解析器的名称一般为 libtree-sitter-<LANGUAGE>.so 所以在Windows上,如果我们要启动elisp的,需要将它改名为 libtree-sitter-elisp.dll。 如果你不想改名,我们可以设置往列表 treesit-load-name-override-list 中添加一条对应的语言和dll的映射。每条记录的格式为 (LANG LIB-NAME [BASE-NAME])。其中 LANG 是语言名称,而 LIB-NAME 是对应的dll名称,BASE_NAME 是库道出的函数名称。默认来讲,每个对应的dll都有一个可以调用的导出函数,命名规则为 tree_sitter_<LANGUAGE> 但是极少数可能出现名称不匹配的规则,所以我们可以添加一条记录。
(add-to-list 'treesit-load-name-override-list
'(elisp "elisp" "tree_sitter_elisp"))之后我们就可以为当前的elisp缓冲区创建一个新的解析器 (treesit-parser-create 'elisp)
语法节点
创建了语法解析器之后,我们就可以利用一些API函数来获取树上节点的属性了。例如可以使用 treesit-node-at 可以获取当前光标位置在树上的节点。
有了节点,我们可以获取节点属性或者进行节点的遍历,对应的函数有:
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比较两个节点是否代表树中的同一个节点
有了这些函数,我们简单实现的实现一个高亮当前光标所在语法单元的功能:
(defun my/mark-current-node()
(interactive)
(let* ((parser (treesit-parser-create 'elisp)) ;;创建解析器
(node (treesit-node-at (point))) ;; 获取当前位置所在的节点
(start (treesit-node-start node)) ;;获取节点对应文本的起始位置
(end (treesit-node-end node))) ;;获取结尾位置
;; 在起点和节点间高亮一个 region
(goto-char start)
(push-mark end t t)))查询
如果我们想要快速的扫描,提取出类似于类定义、类中的属性、方法等内容,依靠遍历整个树还是显得太繁琐了,对于这种查询的需求,我们可以使用 treesitter 提供的查询功能。
它提供了基于树和节点的查询功能:
treesit-search-forward: 从指定节点开始,向前(或向后) 搜索第一个匹配特定条件的节点treesit-search-subtree: 在节点的子树中搜索匹配条件的后代节点treesit-parent-until: 从当前节点向上遍历,直到找到满足条件的祖先节点treesit-induce-sparse-tree: 根据条件在语法树上构建一棵稀疏树,用于快速概览代码结构
虽然上述函数提供了一些搜索节点的方法,但是仍然不够直观。就像数据库,虽然MySQL、或者 Oracle 底层采用的树的结构来提高查找速度,但是在实际操作数据库查找数据时没有人用树的操作来查找。treesitter 在处理诸如查找这一类需求时也提供了对应的高级查找接口:
treesit-query-capture: 利用特定的语法,查找符合条件的接口, 可以接收list列表或者字符串treesit-query-string直接对字符串进行解析和查询treesit-query-validate验证查询语法是否正确
想要使用 tree-query-capture ,需要先了解一下查询的语法。一个查询由一个或者多个模式组成,每个模式都是一个S-表达式,其结构类似于 (type (child) (child) ...)
type表示要查找的节点类型如function_definition或者if_statementchild嵌套的子节点模式,用于进一步限定父节点的结构
例如我们要捕获函数定义所在节点可以这么写
(tree-query-capture node ;;查询开始的节点
'((function_definition (function_name) @func-name)))上述代码表示,我们捕获函数定义的节点,并将它的节点名称命名为 func-name
你可能觉得上述内容比较抽象,看不懂,没关系。实际上它就是treesitter结构中的一个变种,我们了解一段代码在treesitter眼中的样子,也就知道如何编写对应查询条件。在早期的学习使用过程中,我们可以使用命令, treesit-explore-mode,它会将我们当前buffer中在treesitter中解析的内容给列出来,我们只需要点击其中的某项,它就会将对应的代码进行高亮显示。这样我们就可以很容易找到某段代码在treesitter眼中的实际模样。或者开启 treesit-inspect-mode 它会显示当前光标所在位置的节点信息,帮助我们在一堆treesitter 信息中快速找到对应的位置
例如上述高亮当前光标所在语法单元的示例在treesiter眼中,它是这样的
(function_definition ( defun name: (symbol)
parameters: (list ( ))
(special_form ( interactive ))
(special_form ( let*
(list (
(list ( (symbol)
(list ( (symbol)
(quote ' (symbol))
))
))
(comment)
(list ( (symbol)
(list ( (symbol)
(list ( (symbol) ))
))
))
(comment)
(list ( (symbol)
(list ( (symbol) (symbol) ))
))
(comment)
(list ( (symbol)
(list ( (symbol) (symbol) ))
))
))所以根据上述内容,我们如果想获取提取定义的函数名,就可以找 function_definition 类型的节点。根据节点,如果我们想获取对应的函数定义,可以这么写查询条件
(treesit-query-capture root "(function_definition name: (symbol) @func_name)")这段代码查询之后的返回值是 (func_name. node) 结构的列表。因为我们给定的名称就是 func_name。
后面还有一些高级的匹配模式和用法,这里就不介绍了。读者可以自行查阅 treesit-query-capture root 的文档。
练习
本节我们的练习基于上述内容,我们实现一个获取当前buffer中所有函数定义的功能。
我们尝试一下多使用一下相关的函数,以便加深印象。所以本次的实现思路是:
- 函数开始之前先调用
treesit-parser-list遍历一下当前buffer中加载的解析器 - 然后调用
treesit-parser-language获取当前解析器对应的语言 - 如果所有的语言都没有elisp,那么就创建elisp语言的解析器
- 接着通过
treesit-buffer-root-node获取根节点 - 然后通过
treesit-query-captur从根节点往下找,找到所有函数的定义 - 我们遍历返回的结果,取每项的第二个成员,它就是对应的节点对象
- 调用
treesit-node-text获取节点中文本,也就是函数的定义 - 最后打印函数定义,或者存储到一个列表中
最终实现如下:
(defun my/get-all-function-define()
(interactive)
;; 先判断当前是否创建了 elisp 的解析器
(unless (cl-some (lambda(p)
(eq (treesit-parser-language p) 'elisp))
(treesit-parser-list))
(treesit-parser-create 'elisp))
(let* ((root (treesit-buffer-root-node))
(captures (treesit-query-capture root "(function_definition name: (symbol) @func_name)"))
(func-names '()))
(if (not captures)
(message "未找到对应的内容")
(progn
(dolist (capture captures)
(let* ((node (cdr capture))
(name (treesit-node-text node)))
(message "找到函数定义:%s" name)
(push name func-names)))))))总结
本篇我们介绍了treesit 在Emacs中的一些用法,包括与解析器相关的操作、遍历节点、以及在此之上的简单的查询功能。查询还有很多高级的功能,各位读者如果感兴趣可以阅读一下对应的文档。
本篇介绍的函数如下:
treesit-available-p: 检查当前Emacs是否支持 tree-sittertreesit-language-available-p: 检查特定语言的语法定义是否已安装treesit-parser-create: 为当前缓冲区创建一个新的解析器treesit-parser-list: 获取当前缓冲区中的解析器列表treesit-parser-buffer: 获取解析器关联的缓冲区treesit-parser-language: 获取解析器所使用的语言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比较两个节点是否代表树中的同一个节点treesit-query-capture: 利用特定的语法,查找符合条件的接口, 可以接收list列表或者字符串treesit-query-string直接对字符串进行解析和查询treesit-query-validate验证查询语法是否正确
评论 (0)