首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
279 阅读
2
从零开始配置 vim(15)——状态栏配置
161 阅读
3
nvim番外之将配置的插件管理器更新为lazy
146 阅读
4
2018总结与2019规划
138 阅读
5
PDF标准详解(五)——图形状态
117 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
emacs
elisp
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
410
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
1
篇与
的结果
2026-08-20
《elisp 每日一练》——tree-sitter
前面一篇文章中,我们提到 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 验证查询语法是否正确
2026年08月20日
0 阅读
0 评论
0 点赞