38.7 多语言文本解析

有时,某门编程语言的源码中可能会包含其他语言的代码片段; 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-settingstreesit-language-at-point-function 来完成实际工作。 本节末尾会更详细地介绍这些函数与变量。

简而言之,支持多语言的主模式应在调用 treesit-major-mode-setup 之前设置 treesit-primary-parsertreesit-range-settings 以及 treesit-language-at-point-function

获取与设置解析范围

Function: treesit-parser-set-included-ranges parser ranges

该函数将 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 错误。 信号数据中包含具体的错误信息以及尝试设置的范围。

该函数也可用于禁用范围限制。若 rangesnil, 解析器将被设置为解析整个缓冲区。

示例:

(treesit-parser-set-included-ranges
 parser '((1 . 9) (16 . 24) (24 . 25)))
Function: treesit-parser-included-ranges parser

该函数返回为 parser 设置的解析范围。 返回值格式与 treesit-parser-included-rangesranges 参数一致:为形如 (beg . end) 的 cons 单元列表。若 parser 未设置任何范围,返回值为 nil

(treesit-parser-included-ranges parser)
    ⇒ ((1 . 9) (16 . 24) (24 . 25))
Function: treesit-query-range source query &optional beg end

该函数使用 query 匹配 source,并返回捕获节点的范围。 返回值为形如 (beg . end) 的 cons 单元列表, 其中 begend 分别指定文本区域的起始与结束位置。

为方便使用,source 可以是语言符号、解析器或节点。 若为语言符号,函数会在使用该语言的首个解析器的根节点上进行匹配; 若为解析器,则在该解析器的根节点上匹配;若为节点,则直接在该节点上匹配。

参数 query 是用于捕获节点的查询语句 (see 匹配 tree-sitter 节点的模式)。捕获名称无关紧要。 若参数 begend 均不为 nil, 则会限制函数的查询范围。

与其他查询函数类似,若 query 格式错误, 该函数会抛出 treesit-query-error 错误。

在 Lisp 程序中支持多语言

对于通用 Lisp 程序,只需调用以下两个函数, 即可支持混合多种语言的程序源码。

Function: treesit-update-ranges &optional beg end

该函数更新缓冲区中解析器的解析范围。 它会根据 treesit-range-settings, 确保 begend 区间内解析器的范围设置正确。 若省略,beg 默认为缓冲区开头,end 默认为缓冲区结尾。

例如,字体高亮函数会在查询区域内的节点之前调用该函数。

Function: treesit-language-at pos

该函数返回缓冲区位置 pos 处文本所属的语言。 其底层会调用 treesit-language-at-point-function 并返回其执行结果。若 treesit-language-at-point-functionnil,该函数会返回 treesit-parser-list 返回值中首个解析器对应的语言。若缓冲区中无解析器,则返回 nil

在主模式中支持多语言

通常,在一组可混合使用的语言中,会存在一个 宿主语言(host language) 与一个或多个 嵌入式语言(embedded languages)。 Lisp 程序一般先使用宿主语言的解析器解析整个文档, 获取相关信息,再根据这些信息为嵌入式语言设置解析范围, 最后解析嵌入式语言。

以包含 HTMLCSS 与 JavaScript 的缓冲区为例。 Lisp 程序会先用 HTML 解析器解析整个缓冲区, 随后向解析器查询 style_elementscript_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)))))
Function: treesit-range-rules &rest query-specs

该函数用于设置 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-attreesit-local-parsers-on 获取。

treesit-update-ranges 使用 query 确定如何为嵌入式语言的解析器设置范围。 它在宿主语言解析器中执行 query 查询, 计算捕获节点覆盖的范围,并将这些范围应用到嵌入式语言解析器上。

query 为函数,则无需附带任何 keyword/value 对。 该函数应接收 startend 两个参数, 并在当前缓冲区的 startend 区间内为解析器设置范围。 该函数也可以设置覆盖 startend 区间的更大范围。

Variable: treesit-range-settings

该变量协助 treesit-update-ranges 更新缓冲区中解析器的解析范围。它是一个由多个 setting 组成的列表, 单个 setting 的具体格式视为内部实现。 应使用 treesit-range-rules 生成该变量可接受的值。

Variable: treesit-language-at-point-function

该变量的值应为一个单参数函数,参数 pos 为缓冲区位置, 返回 pos 处缓冲区文本所属的语言。 该变量由 treesit-language-at 使用。

Function: treesit-local-parsers-at &optional pos language

该函数返回当前缓冲区中 pos 位置的所有局部解析器。 pos 默认为光标位置。

局部解析器是指仅解析由叠加层标记的有限区域、 且 treesit-parser 属性非 nil 的解析器。 若 language 不为 nil,则仅返回该语言对应的解析器。

Function: treesit-local-parsers-on &optional beg end language

该函数与 treesit-local-parsers-at 功能相同, 但返回 begend 区间内的局部解析器,而非光标位置。

begend 默认为缓冲区的全部可访问区域。