Helix 注入查询(injections.scm)详解:语言捕获、设置与谓词的完整指南
本文基于 Helix 官方指南 injection.md,讲解如何为 Helix 编写 tree-sitter 语言注入(injection)查询:如何用 @injection.content 等捕获把某个语法节点标记为另一种语言,如何用 #set! 设置和 #match? 等谓词精细控制注入行为,以及注入区域如何进一步影响缩进、文本对象与注释行为。读完后你可以为自己使用的语言(或嵌入型语言)编写可运行的 injections.scm,并理解 Helix 内部如何解析这些查询。
注入不只是高亮:Helix 的扩展能力
编写注入查询的核心目的是:把语法树中的某个节点按另一种语言高亮。例如在 HTML 中按 JavaScript 高亮 <script> 内容,在 Markdown 中按对应语言高亮围栏代码块。
在 tree-sitter 标准的语言注入机制之上,Helix 增加了自己的扩展(下文标注 extension),提供更强的控制力。
更重要的是,注入在 Helix 中驱动的远不止高亮。根据 injection.md 的说明:在一个注入区域内,Helix 还会使用被注入语言自身的缩进规则、文本对象和注释标记——因此当你编辑 HTML <script> 标签内的 JavaScript 时,自动缩进和 gc 注释操作都按 JavaScript 的规则执行。这一行为可以从源码得到印证:indents 逻辑 中明确处理了“位于注入层内”的缩进计算:
// Inside an injection layer indent with that language's query and tree,
// then shift the whole result by the injection's base indent.
let injected = (layer != syntax.root_layer() ...);
let query = injected.unwrap_or(query);
即:当光标所在层不是文档根语法树时,缩进会改用被注入语言自己的 indents.scm 查询和语法树来计算,再通过 injection_base_level 把结果整体偏移为外层文档的基线缩进。同一语言注入(如 Rust 宏的 token-tree 自注入)则不做特殊处理,直接沿用原语言缩进。
基础示例:两条最简单的注入查询
以下示例来自官方指南原文,可直接作为起点:
示例 1:把 Nix 中所有字符串按 bash 高亮
((string_expression (string_fragment) @injection.content)
(#set! injection.language "bash"))
这条查询捕获 string_fragment 节点,并用 #set! injection.language "bash" 强制其内容按 bash 语言解析。
示例 2:借助专用 "comment" 语言高亮注释中的链接和 TODO 关键词
((comment) @injection.content
(#set! injection.language "comment"))
这里复用了 Helix 仓库自带的 comment 语言查询(该语言专门把 TODO、FIXME、URL 等标记成独立的语法节点),使得任何语言的注释都能获得统一的关键词高亮。
这两条查询应放在语言目录下的 injections.scm 中。从 compile_syntax_config 可以看到加载流程:
let highlight_query_text = read_query(name, "highlights.scm");
let injection_query_text = read_query(name, "injections.scm");
let local_query_text = read_query(name, "locals.scm");
let config = SyntaxConfig::new(grammar, &highlight_query_text, &injection_query_text, &local_query_text)
即 Helix 在编译某个语言时会同时读取 highlights.scm、injections.scm、locals.scm 三份查询并一起编译,其中任何一份缺失都会被当作空查询。内置的各语言注入查询全部位于 runtime/queries/ 下各语言目录,例如 markdown 的 injections.scm。
捕获类型(Capture Types)
标准捕获
@injection.language(standard):被捕获的节点中包含用于高亮的语言名称,该名称用来确定@injection.content捕获节点按哪种语言渲染。典型用法是捕获 Markdown 围栏代码块信息串中的语言标签。@injection.content(standard):标记“要按@injection.language(等)方式重新解析高亮”的内容节点本身。
Helix 扩展捕获
@injection.filename(extension):被捕获的节点中包含一个文件名(或其扩展名),扩展名只要被 Helix 已知(来自默认随发行版分发的languages.toml以及用户自定义语言),就把@injection.content按对应语言高亮。适合“按文件后缀推断内嵌语言”的场景。@injection.shebang(extension):被捕获的节点中包含一个 shebang(#!行),用它来决定按哪种语言高亮。同样基于默认与用户languages.toml中定义的 shebang 映射。
一个同时用到 @injection.shebang 与 @injection.language 的真实例子来自 markdown/injections.scm:
(fenced_code_block
(code_fence_content) @injection.shebang @injection.content
(#set! injection.include-unnamed-children))
(fenced_code_block
(info_string
(language) @injection.language)
(code_fence_content) @injection.content
(#set! injection.include-unnamed-children))
第一条匹配“无语言标注、但内容首行带 shebang”的代码块;第二条匹配信息串中写了语言名的代码块。二者互斥,共同覆盖了 Markdown 围栏代码块的注入场景。
设置项(Settings)
以下设置通过查询中的 #set! 谓词写入,作用于对应的捕获节点:
| 设置项 | 来源 | 作用 |
|---|---|---|
injection.combined |
标准 | 表示树中所有匹配的节点应合并为一个嵌套文档整体解析(多节点内容共享同一注入树) |
injection.language |
标准 | 强制被捕获内容按给定语言名高亮 |
injection.include-children |
标准 | 内容节点的全部文本(含所有子节点文本)都参与重新解析;默认情况下子节点文本会被排除在注入文档之外 |
injection.include-unnamed-children |
Helix 扩展 | 与 injection.include-children 相同,但只包含无名(unnamed)子节点,有名字的子节点仍被排除 |
injection.include-unnamed-children 是相对标准的 injection.include-children 的细化控制:在大多数 tree-sitter 语法中,文本内容存放在匿名(无名)节点里,因此只需包含无名子节点即可拿到完整文本,同时避免重复处理已命名的结构节点。该谓词正是 CHANGELOG 记录中新增的 Helix 扩展。
真实用例:markdown/injections.scm 中 html_block 的注入:
((html_block) @injection.content
(#set! injection.language "html")
(#set! injection.include-unnamed-children)
(#set! injection.combined))
这里 injection.combined 确保文档里所有 html_block 节点合并进同一个 HTML 嵌套文档再解析——这对跨节点的结构(如多段拼接的 HTML)尤为关键。类似的用法大量存在于 just/injections.scm(合并多行 recipe 以便 bash 解析跨行结构)、elixir/injections.scm 等内置查询中。
谓词(Predicates)
以下谓词用于给捕获附加条件或约束,是 tree-sitter 标准谓词:
#eq?:第一个参数(一个捕获)必须等于第二个参数(捕获或字符串)。例如(#eq? @injection.language "rust")可筛选语言标签。#match?:第一个参数(一个捕获)必须匹配第二个参数给出的正则表达式(字符串)。例如可按前缀模式批量匹配语言名。#any-of?:第一个参数(一个捕获)必须属于其余参数(字符串列表)之一,适合白名单式的多值匹配。
这些谓词常与 @injection.language 捕获组合,用来限定“信息串匹配到哪些语言才启用注入”,从而避免把未知标识符误当作语言名。
底层机制速览
结合源码,可以梳理出 Helix 处理注入查询的完整链路(从源码结构看):
- 编译期:compile_syntax_config 从语言目录读取
injections.scm,与 highlights、locals 查询一并编译进SyntaxConfig;缺少对应语法共享库时该语言会被整体跳过。 - 解析期:高亮器(tree-house 的
SyntaxConfig)根据注入查询在文档上建立注入层(injection layer),每层持有内嵌语言自己的语法树。 - 编辑期:缩进、文本对象、注释等操作会先定位光标/选区所在的注入层。indent.rs 显示缩进会切换到注入层的查询与树计算,再由 injection_base_level 以“注入内容首个非空白字符行”为基准做整体偏移;文档侧的语言解析逻辑见 document.rs 中对注入的引用。
- 语言识别:与注入查询不同,
LanguageConfiguration中的injection_regex(见 syntax/config.rs)用于按文本内容推断文档级语言,属于另一套机制,不要与节点级注入混淆。
实践建议:如何为某语言新增注入
- 定位语言目录
runtime/queries/<language>/,创建或编辑injections.scm(每个内置语言的注入查询都遵循这一布局,如 bash/injections.scm)。 - 用
@injection.content标记内嵌内容节点;语言名固定时用#set! injection.language "<lang>",动态时改用@injection.language/@injection.filename/@injection.shebang捕获。 - 若内嵌内容依赖子节点文本(常见于把“容器节点”整体作为注入文档),加上
#set! injection.include-unnamed-children(或标准的injection.include-children)。 - 若同一语法树内多处片段必须作为一个整体解析(跨节点配对、多行命令等),加上
#set! injection.combined。 - 需要按模式筛选语言标签时,组合
#eq?/#match?/#any-of?谓词。
编写完整的新语言支持(含 languages.toml 条目、查询文件与测试)可进一步参考 添加语言指南;与本文相邻的还有 高亮查询指南、文本对象查询指南 和 locals 查询指南,可对照阅读。
小结
Helix 的注入体系以 tree-sitter 标准的 @injection.language / @injection.content 为骨架,用 @injection.filename、@injection.shebang 两个扩展捕获覆盖了“按文件后缀/shebang 推断内嵌语言”的常见场景,并用 injection.include-unnamed-children 对“子节点文本是否参与重新解析”做了更细的粒度控制。由于注入区域同时接管缩进、文本对象与注释行为,一份写好的 injections.scm 带来的收益是编辑体验层面而非仅仅是视觉层面的——这正是理解并善用 injection.md 所描述的这些机制的价值所在。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00