有时,某门编程语言的源码中可能会包含其他语言的代码片段; HTML + CSS + JavaScript 就是一个典型例子。 这种情况下,使用不同语言编写的文本片段需要分配不同的解析器。 传统上,这一需求通过 narrowing 实现。尽管 tree-sitter 可以配合 narrowing 使用 (see narrowing),但更推荐的方式是为解析器指定缓冲区文本的作用区域(即范围)。 本节介绍用于为解析器设置和获取解析范围的函数。
通常在涉及多种语言时,会存在一门 “主语言(primary)” 或 “宿主语言(host)”。 该语言对应的解析器——即 主解析器(primary parser),会解析整个缓冲区。 其他语言的解析器则是 “嵌入式(embedded)” 或 “客语言(guest)” 解析器,仅作用于缓冲区的部分区域。 主解析器生成的解析树通常用于确定嵌入式解析器的工作范围。
主模式应在调用 treesit-major-mode-setup 之前,将 treesit-primary-parser
设置为主解析器,以便 Emacs 能为主解析器正确配置字体高亮及其他功能。
Lisp 程序在缓冲区中使用解析器之前,应调用 treesit-update-ranges
确保每个解析器的范围正确,并调用 treesit-language-at
判断某个位置的文本所属的语言。这两个函数无法独立工作;
它们需要主模式设置 treesit-range-settings 和
treesit-language-at-point-function 来完成实际工作。
本节末尾会更详细地介绍这些函数与变量。
简而言之,支持多语言的主模式应在调用
treesit-major-mode-setup 之前设置
treesit-primary-parser、treesit-range-settings
以及 treesit-language-at-point-function。
该函数将 parser 设置为仅在 ranges 范围内工作。
parser 只会读取指定范围的文本。ranges 中的每个范围
均为形如 (beg . end) 的 cons 单元。
ranges 中的范围必须按顺序排列且不能重叠。 用伪代码表示即:
(cl-loop for idx from 1 to (1- (length ranges))
for prev = (nth (1- idx) ranges)
for next = (nth idx ranges)
should (<= (car prev) (cdr prev)
(car next) (cdr next)))
如果 ranges 违反该约束,或出现其他错误,
该函数会触发 treesit-range-invalid 错误。
信号数据中包含具体的错误信息以及尝试设置的范围。
该函数也可用于禁用范围限制。若 ranges 为 nil,
解析器将被设置为解析整个缓冲区。
示例:
(treesit-parser-set-included-ranges parser '((1 . 9) (16 . 24) (24 . 25)))
该函数返回为 parser 设置的解析范围。
返回值格式与 treesit-parser-included-ranges 的
ranges 参数一致:为形如 (beg . end)
的 cons 单元列表。若 parser 未设置任何范围,返回值为 nil。
(treesit-parser-included-ranges parser)
⇒ ((1 . 9) (16 . 24) (24 . 25))
该函数使用 query 匹配 source,并返回捕获节点的范围。
返回值为形如 (beg . end) 的 cons 单元列表,
其中 beg 与 end 分别指定文本区域的起始与结束位置。
为方便使用,source 可以是语言符号、解析器或节点。 若为语言符号,函数会在使用该语言的首个解析器的根节点上进行匹配; 若为解析器,则在该解析器的根节点上匹配;若为节点,则直接在该节点上匹配。
参数 query 是用于捕获节点的查询语句
(see 匹配 tree-sitter 节点的模式)。捕获名称无关紧要。
若参数 beg 与 end 均不为 nil,
则会限制函数的查询范围。
与其他查询函数类似,若 query 格式错误,
该函数会抛出 treesit-query-error 错误。
对于通用 Lisp 程序,只需调用以下两个函数, 即可支持混合多种语言的程序源码。
该函数更新缓冲区中解析器的解析范围。
它会根据 treesit-range-settings,
确保 beg 至 end 区间内解析器的范围设置正确。
若省略,beg 默认为缓冲区开头,end 默认为缓冲区结尾。
例如,字体高亮函数会在查询区域内的节点之前调用该函数。
该函数返回缓冲区位置 pos 处文本所属的语言。
其底层会调用 treesit-language-at-point-function
并返回其执行结果。若 treesit-language-at-point-function
为 nil,该函数会返回 treesit-parser-list
返回值中首个解析器对应的语言。若缓冲区中无解析器,则返回 nil。
通常,在一组可混合使用的语言中,会存在一个 宿主语言(host language) 与一个或多个 嵌入式语言(embedded languages)。 Lisp 程序一般先使用宿主语言的解析器解析整个文档, 获取相关信息,再根据这些信息为嵌入式语言设置解析范围, 最后解析嵌入式语言。
以包含 HTML、CSS 与 JavaScript 的缓冲区为例。
Lisp 程序会先用 HTML 解析器解析整个缓冲区,
随后向解析器查询 style_element 与 script_element 节点,
它们分别对应 CSS 与 JavaScript 文本。
之后再将 CSS 与 JavaScript 解析器的范围
设置为对应节点所覆盖的区间。
给定一个简单的 HTML 文档:
<html>
<script>1 + 2</script>
<style>body { color: "blue"; }</style>
</html>
Lisp 程序会先用 HTML 解析器解析, 再为 CSS 与 JavaScript 解析器设置范围:
;; 创建解析器 (setq html (treesit-parser-create 'html)) (setq css (treesit-parser-create 'css)) (setq js (treesit-parser-create 'javascript))
;; 设置 CSS 解析范围
(setq css-range
(treesit-query-range
'html
'((style_element (raw_text) @capture))))
(treesit-parser-set-included-ranges css css-range)
;; 设置 JavaScript 解析范围
(setq js-range
(treesit-query-range
'html
'((script_element (raw_text) @capture))))
(treesit-parser-set-included-ranges js js-range)
Emacs 在 treesit-update-ranges 中自动完成这一流程。
支持多语言的主模式应设置 treesit-range-settings,
使 treesit-update-ranges 能够自动执行该流程。
主模式应使用辅助函数 treesit-range-rules
生成可赋值给 treesit-range-settings 的值。
下例中的设置与上述操作直接对应。
(setq treesit-range-settings
(treesit-range-rules
:embed 'javascript
:host 'html
'((script_element (raw_text) @capture))
:embed 'css
:host 'html
'((style_element (raw_text) @capture))))
;; 多语言主模式应始终设置
;; `treesit-language-at-point-function'(详见说明)
(setq treesit-language-at-point-function
(lambda (pos)
(let* ((node (treesit-node-at pos 'html))
(parent (treesit-node-parent node)))
(cond
((and node parent
(equal (treesit-node-type node) "raw_text")
(equal (treesit-node-type parent) "script_element"))
'javascript)
((and node parent
(equal (treesit-node-type node) "raw_text")
(equal (treesit-node-type parent) "style_element"))
'css)
(t 'html)))))
该函数用于设置 treesit-range-settings。
它负责编译查询语句及其他后续处理,并输出
treesit-range-settings 可接受的值。
它接收一系列 query-spec,每个 query-spec 是在 query 前附带零个或多个 keyword/value 对。 每个 query 可以是字符串、S 表达式、编译后形式的 tree-sitter 查询, 或是一个函数。
若 query 为 tree-sitter 查询,则其前面应包含两个
keyword/value 对::embed 关键字指定嵌入式语言,
:host 关键字指定宿主语言。
若为查询指定 :local 关键字且值为 t,
则该查询设置的范围会使用独立的局部解析器;
否则该范围会与同一语言的其他范围共用一个解析器。
默认情况下,解析器会将其范围视为连续整体, 而非多个相互独立的片段。因此,若嵌入式范围在语义上是独立片段, 则应使用下文所述的局部解析器处理。
设置在某一范围上的局部解析器可通过
treesit-local-parsers-at 与 treesit-local-parsers-on 获取。
treesit-update-ranges 使用 query
确定如何为嵌入式语言的解析器设置范围。
它在宿主语言解析器中执行 query 查询,
计算捕获节点覆盖的范围,并将这些范围应用到嵌入式语言解析器上。
若 query 为函数,则无需附带任何 keyword/value 对。 该函数应接收 start 与 end 两个参数, 并在当前缓冲区的 start 至 end 区间内为解析器设置范围。 该函数也可以设置覆盖 start 至 end 区间的更大范围。
该变量协助 treesit-update-ranges
更新缓冲区中解析器的解析范围。它是一个由多个 setting 组成的列表,
单个 setting 的具体格式视为内部实现。
应使用 treesit-range-rules 生成该变量可接受的值。
该变量的值应为一个单参数函数,参数 pos 为缓冲区位置,
返回 pos 处缓冲区文本所属的语言。
该变量由 treesit-language-at 使用。
该函数返回当前缓冲区中 pos 位置的所有局部解析器。 pos 默认为光标位置。
局部解析器是指仅解析由叠加层标记的有限区域、
且 treesit-parser 属性非 nil 的解析器。
若 language 不为 nil,则仅返回该语言对应的解析器。
该函数与 treesit-local-parsers-at 功能相同,
但返回 beg 至 end 区间内的局部解析器,而非光标位置。
beg 与 end 默认为缓冲区的全部可访问区域。