为 Slint 语言打造 tree-sitter 语法解析:grammar 设计、Rust 宏注入与多编辑器集成
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
Slint 是面向 Rust、C++、JavaScript 与 Python 的声明式 GUI 工具包,其.slint语言需要被各类编辑器正确高亮、缩进与折叠。tree-sitter-slint正是为 Slint 语言提供 tree-sitter 语法支持的开源实现:它既发布为可独立使用的 Rust crate(暴露i_tree_sitter_slint::LANGUAGE),也可作为通用解析器被 vim、helix、Neovim、Zed 等编辑器接入,甚至能注入到 Rust 源码中让slint!宏内部的 UI 代码获得原生语法高亮。本文以仓库中的 editors/tree-sitter-slint/README.md 为主线,结合 grammar.js、外部扫描器与测试脚本,完整讲解该语法支持的原理、配置与集成方法,读完后你可以自己动手在 Neovim 中配置 Rust 宏注入,并理解如何为其他编辑器贡献同样的能力。
什么是 tree-sitter,为什么 Slint 需要它
按照 tree-sitter 官方页面的定义(README 开篇引用):
Tree-sitter 是一个解析器生成工具,同时也是一个增量解析库。它能为一个源文件构建具体的语法树(concrete syntax tree),并且在源文件被编辑时高效地更新这棵语法树。
这段话包含了两个对编辑器体验至关重要的特性:
- 具体的语法树(CST):不同于只关心"语法是否正确"的抽象语法树(AST),tree-sitter 生成的 CST 保留了注释、空白与每一个语法片段的精确位置,这使高亮、缩进、折叠、代码块配对等编辑功能可以精确命中每一个 token。
- 增量解析:当用户敲入一个字符时,tree-sitter 不必重新解析整个文件,而只需重解析被影响的局部区域,因此即使对几百 KB 的
.slint文件也能保持流畅的实时反馈。
Slint 项目正是利用这两点,为.slint语言维护了这份语法描述,并开放给所有支持 tree-sitter 的编辑器使用。README 明确写到:"Use with vim/helix/... other editors",即这份语法不绑定任何特定编辑器,任何能加载 tree-sitter grammar 的编辑器都能获得对 Slint 语言的解析能力。
仓库中的配套文件与 Rust crate
在 editors/tree-sitter-slint 目录下,除了本文依据的 README,还包含完整的语法实现:
| 文件 | 作用 |
|---|---|
| grammar.js | 用 tree-sitter 的 DSL 描述 Slint 语言的完整语法规则 |
| src/scanner.c | 外部扫描器(external scanner),处理普通正则无法表达的嵌套块注释 |
| tree-sitter.json | 语法包元数据:名称、scope、文件类型、injection 正则、各语言 binding 开关 |
| test/corpus/ | 语料测试(empty.txt、events.txt、rust_attr.txt、string_escape.txt、struct_field_defaults.txt) |
| run_tests.sh | 一键测试脚本:生成解析器、校验编辑器 query、跑全部语料 |
| test-to-corpus.py | 把仓库中的.slint测试/示例自动转换为 tree-sitter 语料 |
| CONTRIBUTING.md | 贡献指南与当前测试覆盖状态清单 |
其中 README 强调了一个关键点:这个目录同时也是一个独立的 Cargo 包,它把生成的 tree-sitter 语言暴露为i_tree_sitter_slint::LANGUAGE。也就是说,Rust 开发者不必经过 C/C++ 编译链,就能在自己的 Rust 程序里以i_tree_sitter_slint::LANGUAGE直接拿到 Slint 语言的解析器句柄,用于构建语法树、实现自己的高亮器、文档工具或代码分析器。
在 tree-sitter.json 中可以确认语法包的完整标识:
name:slint,scope:source.slint,file-types:["slint"];injection-regex:^slint$——这是后续"注入 Rust"功能的基础,注入方用语言名slint即可命中;bindings中c、go、node、python、rust、swift均为true,说明这套语法可由 tree-sitter CLI 生成多语言 binding。
语法规则设计:从顶层定义到表达式
grammar.js 是语法的"宪法",共约 1028 行,完整覆盖了 Slint 语言当前的核心语法面。理解它的分层结构,有助于你排查编辑器高亮异常或提交新测试。整个规则树大致分为以下几层:
顶层与定义层。sourcefile由若干_definition组成,而_definition又分为三类:import_statement(导入语句)、_export(导出)、_local_type(本地类型)。本地类型则涵盖:
component_definition:同时支持新语法component Foo { ... }与旧语法Foo := Rectangle { ... },并支持inherits修饰符;struct_definition与enum_definition:同样兼容旧式:=写法;global_definition(全局单例)与interface_definition(接口声明);rust_attr:@rust-attr(...)属性,允许嵌套括号与字符串参数,见语料 test/corpus/rust_attr.txt 中的示例。
组件块内层。block内的_block_statement允许的属性与子语句包括:property(属性声明,支持@deprecated、@shadowable属性前缀与private/in/out/in-out可见性)、binding_alias(<=>双向绑定别名)、callback、callback_alias、callback_event(name(...) => { ... }回调设置)、changed_event(changed prop => { ... })、for_loop、if_statement、match_element、states_definition、transitions_definition、slot_declaration/slot_assignment/slot_forwarding、function_definition以及嵌套的component等。其中events.txt语料专门验证了changed作为上下文关键字(contextual keyword)在"回调名"与"changed 事件"两种语境下的消歧:changed => {}、changed(test) => {}被解析为callback_event,而changed value => {}被解析为changed_event,参见 test/corpus/events.txt。
表达式层。expression采用prec.right/prec.left精确控制优先级,覆盖一元(!、-、+)、二元算术(+、-、*、/)、比较(>、<、>=、<=、==、!=)、逻辑(&&、||)、三元(? :)、成员访问(.)、索引([])、函数调用、闭包((x) => ...)以及 Slint 特有的语法:
@tr(...)翻译调用、@markdown(...)、@keys(...)快捷键描述;@linear-gradient/@radial-gradient/@conic-gradient渐变调用(gradient 内部颜色参数以argument加可选expression的形式表达,允许"无分隔符的任意表达式",这也是 grammar 中登记conflicts的原因之一);@image-url(...)图片引用,支持nine-slice(...)九宫格参数;- 相邻字符串表达式
adjacent_string_expression(字符串字面量拼接)。
字面量层。_basic_value覆盖了 Slint 的全部内置字面量:整数、浮点、布尔、字符串、颜色(#rrggbb形式)、物理长度(phx单位)、长度(px/cm/mm/in/pt)、时长(ms/s)、角度(deg/grad/turn/rad)、百分比(%)、相对字号(rem)以及一整套缓动函数关键字(linear、ease-in-out、cubic-bezier(...)等)。类型系统则区分了builtin_type_identifier(int、float、bool、string、color、brush、length、duration、image等 15 种)与user_type_identifier,并支持type_list与内联struct_block类型。这些细节直接决定编辑器能否正确为12phx、2s、#ff0000这类 token 分类着色。
外部扫描器:让块注释支持嵌套
tree-sitter 的注释通常作为"extras"在词法阶段用正则匹配,但Slint 的块注释支持平衡嵌套——/* /* */ */是合法写法,普通正则无法表达这种深度嵌套。因此 grammar.js 中把block_comment声明为externals,并交由 src/scanner.c 这个 C 语言外部扫描器处理。
扫描器的逻辑非常直观:跳过空白后检查是否以/*开头,然后维护一个depth计数器,遇/*则depth++,遇*/则depth--,直到depth == 0时标记 token 结束;若一直读到文件末尾仍未闭合,则返回 false(未终结的注释不产生 token)。正如扫描器注释所说明的,OCaml 与 Rust 的 tree-sitter grammar 也采用同样的外部扫描器方案解决嵌套注释问题。这一设计保证了即使注释内部存在大量/*、*/或代码片段,解析器也不会误解语法树结构,进而避免编辑器高亮"错位"。
测试与验证:从仓库语料到语料测试
一个语法解析器是否可靠,最终要靠测试来保证。tree-sitter-slint的测试策略很有特色:直接把仓库自身积累的.slint文件转化为 tree-sitter 语料。
run_tests.sh 的完整流程是:
tree-sitter generate生成解析器,tree-sitter build立即编译,尽早暴露 grammar 错误;- 用生成的解析器逐一校验 editors/zed/languages/slint 下的所有
*.scm编辑器 query(highlights、folds、indents、injections 等),并分别对 tests/cases、examples、demos 中全部.slint文件执行tree-sitter query,确保 grammar 改动不会悄悄破坏 Zed 的查询规则; - 清空
test/corpus/gen后,通过 test-to-corpus.py 把tests/cases、examples、demos下每个目录中的.slint文件转换为标准语料格式(剥离 Copyright/SPDX 头与注释内的代码块,追加预期的(sourcefile ...)输出),按目录名聚合写入gen/下的 txt 文件; - 先以
-u更新可自动修复的测试,再执行tree-sitter test跑完整断言; - 最后用
grep -nC10 ERROR确保生成的语料中不存在 ERROR 节点,避免 tree-sitter CLI 行为变化把错误静默吞掉。
由此可见,Slint 仓库中数以千计的测试用例与示例文件实际上构成了语法解析器的"活体语料库",任何语法回归都会在tree-sitter test中被捕捉。手动维护的 test/corpus/ 目录则用于精确锁定特定语法点(如事件、rust-attr、字符串转义、结构体字段默认值)。
在 Neovim 中把 Slint 注入 Rust 的slint!宏
这是 README 中最核心的实操内容。Slint 的 Rust API 允许在 Rust 源码中直接书写slint!宏,宏内部就是一段.slint语言文本。如果不做特殊配置,编辑器只会把宏体内的内容当作普通字符串高亮。利用 tree-sitter 的 injection 机制,可以让slint!宏体获得完整的 Slint 语法高亮。
在 Neovim 中,配合nvim-treesitter插件,步骤如下:
- 执行
:TSEditQueryUserAfter injections rust创建(或编辑)用户级 Rust 注入配置文件; - 把 README 提供的注入 query 完整粘贴到该文件中:
;; Inject the slint language into the `slint!` macro: (macro_invocation macro: [ ( (scoped_identifier path: (_) @_macro_path name: (_) @_macro_name ) ) ((identifier) @_macro_name @macro_path) ] ((token_tree) @injection.content (#eq? @_macro_name "slint") (#eq? @_macro_path "slint") (#offset! @injection.content 0 1 0 -1) (#set! injection.language "slint") (#set! injection.combined) (#set! injection.include-children) ) )这段 query 的含义值得逐条拆解:
macro_invocation匹配 Rust 中的宏调用节点,macro:字段既匹配slint::slint!这种带路径的scoped_identifier(捕获path与name),也匹配裸写的slint!(identifier同时绑定到@_macro_name与@_macro_path两个捕获);(token_tree) @injection.content把宏的参数 token 树标记为注入内容;(#eq? @_macro_name "slint")与(#eq? @_macro_path "slint")双重校验:只有当宏名与路径均为slint时才注入,避免误伤其他名为 slint 的宏;(#offset! @injection.content 0 1 0 -1)把注入内容首行偏移 1 列、末行偏移 -1 列,从而剥掉slint!宏的左右边界字符,让解析器看到的是纯.slint代码;(#set! injection.language "slint")指定注入的语言——该值需要与 tree-sitter.json 中injection-regex: "^slint$"对应;(#set! injection.combined)允许跨多个节点合并注入(应对宏跨多行的情况),(#set! injection.include-children)则让子节点一并纳入。
配置完成后,重新加载 Rust 文件,slint!宏内部的 UI 声明就会获得与.slint文件完全一致的高亮、折叠与缩进体验。
更多编辑器:vim、helix 与仓库内的 Zed 集成
README 指出该语法可用于 "vim/helix/... other editors",并欢迎大家提交"在其他编辑器中实现同样注入"的 PR。实际上,Slint 仓库自身就提供了一个完整的参考实现:Zed 编辑器集成位于 editors/zed/languages/slint,包含:
- config.toml:声明语言名
Slint、语法slint、文件后缀匹配(Cargo.lock与slint)、行注释//、以及括号配对/自动闭合规则({}、[]、()、<>、字符串与注释引号); - highlights.scm:308 行高亮规则,把注释、字符串、转义序列、颜色、布尔、数值(含
phx、deg、%、rem等带单位字面量)、可见性/纯度修饰符、内置类型、关键字等分类映射到 Zed 的捕获名; - 配套的
brackets.scm、folds.scm、indents.scm、injections.scm、locals.scm查询文件。
这组文件恰好演示了"同一份 grammar,服务于不同编辑器"的模式:tree-sitter 只负责解析,具体的着色、折叠、缩进策略由每个编辑器各自的.scmquery 定义。对于 vim/helix 用户,只需把该 grammar 注册为slint语言(scope 为source.slint,文件类型*.slint)即可获得基础解析能力;若要在 Rust 中注入,可参照上文 Neovim 的 query 思路为各自的编辑器方言改写。
贡献指南与当前覆盖状态
如果你发现.slint代码在编辑器中解析出错,或某个新语法点尚未覆盖,可以参考 CONTRIBUTING.md 参与贡献:
- 需要
tree-sitterCLI 工具,先用tree-sitter generate生成解析器; - 测试使用
tree-sitter test命令执行(该仓库的测试当前未接入 CI 自动运行,主要靠人工/脚本触发); - 格式风格可通过
tree-sitter test -u自动更新全部测试,但此命令会让失败测试也"通过",需谨慎使用; - 该文档保留了一份勾选清单,记录语法的覆盖状态:例如组件(组件声明、可见性、属性设置、子组件、命名/条件组件)、语句(导入、全局单例、导出、动画、状态、过渡)、表达式(三元、链式三元、字符串、图片、空表达式)等大多已覆盖,而嵌套注释、匿名结构体、双向绑定、数组/颜色/画刷表达式、带参数的回调设置等仍在清单上等待补全。
值得注意的是,grammar.js 中已实现的部分(如anonymous struct的anon_struct_block、<=>双向绑定、implement ... <=> ...接口实现语句)与 CONTRIBUTING 清单中的未勾选项并不完全一致,说明清单可能滞后于实现——以实际 grammar 与tree-sitter test的结果为准。语法包本体采用 GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 多重许可(见 tree-sitter.json),而 grammar.js 与 src/scanner.c 单独标注为 MIT 许可,贡献时需遵守仓库 LICENSE.md 的相应条款。
总结
tree-sitter-slint用一套 grammar 定义(grammar.js)加一个外部扫描器(src/scanner.c),为 Slint 语言提供了覆盖顶层定义、组件、属性、表达式、字面量与嵌套注释的完整解析能力;借助i_tree_sitter_slint::LANGUAGE可被 Rust 程序直接消费;通过 injection query 可以无缝嵌入 Rust 的slint!宏,让 vim、helix、Neovim、Zed 等编辑器获得一致的语法高亮体验。对编辑器作者而言,这个仓库既是一个开箱即用的语法包,也是一份展示"如何为一种声明式 UI 语言编写健壮 tree-sitter 语法并持续用真实代码语料回归"的完整范例。
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考