Helix 的 locals.scm 查询:用作用域解析让局部变量高亮区别于全局变量
Helix 基于 tree-sitter 的语法高亮体系由多类查询文件分工协作,其中 locals.scm 负责教编辑器"理解"变量的作用域与定义,使得 @local.reference 能解析到 @local.definition 并继承后者的语义高亮类。本文基于官方指南 Adding locals queries 逐条讲解三类核心捕获、discard 捕获的优先级规则以及定义/引用匹配机制,并结合 helix-core 的编译流程与 runtime/queries 下上百个语言的真实查询文件,给出可直接参考的编写方法。
locals.scm 的定位:高亮的"第二阶段"
在理解 locals.scm 之前,需要先明确它与 highlights.scm 的关系,这也是 指南原文 末尾专门强调的核心原则:
- locals 系统是"伴随运行"而非"替代":
highlights.scm永远决定一个节点的基线高亮;locals.scm只在一个@local.reference成功解析到某个@local.definition时,才对该次解析覆写高亮。 - 独立作用域、独立优先级:locals 在独立的 pass 中解析,拥有自己的优先级(precedence)体系,与
highlights.scm的捕获优先级互不干扰。 - discard 捕获不污染基线高亮:
locals.scm中所有非@local.*的捕获(即 discard)只影响 locals 的解析结果,对highlights.scm的结果没有任何副作用。
这一设计带来的实际效果是:一个函数参数(定义处标记为 variable.parameter)在其作用域内被引用时,引用点不再只是 highlights.scm 赋予的普通 identifier 高亮,而是继承 variable.parameter 高亮类——参数、可变变量、命名空间引用等于是"看得见"的。
查询文件的组织方式
按照 官方指南 的约定,贡献给 Helix 的 locals 查询文件应放置在:
runtime/queries/{language}/locals.scm
其中 {language} 是 languages.toml 中定义的 language id。当前仓库中,runtime/queries 目录下已有 100 个以上语言提供了 locals.scm,覆盖 Rust、Python、C、Go、TypeScript、Nix 等主流语言。
与 highlights.scm 等其他查询文件一样,locals.scm 可以在首行通过 ; inherits: <lang> 复用另一份查询文件。这是语言之间存在包含关系时的常用手段,例如 TypeScript 与 ECMAScript 共享大量语法节点。
从源码结构看,这份文件在 Helix 启动编译语言配置时就被读取:helix-core/src/syntax.rs 中的 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");
随后三者一起交给 SyntaxConfig::new 编译。也就是说,locals 查询与高亮查询在同一时机、同一配置对象中完成编译,但各自维护独立的捕获集合与优先级,这正对应了指南中"独立 pass、独立优先级"的描述。若 locals.scm 不存在或为空,read_query 返回空文本,该语言只是没有 locals 能力,不会报错——这正是它作为可选增强层的设计。
捕获一:@local.scope 作用域边界
@local.scope 标记一个节点为作用域边界。规则是:一个定义只对"出现在同一作用域或更内层作用域"的引用可见。典型的作用域节点是函数体与代码块。
指南给出的最小示例:
[
(function_definition)
(block)
] @local.scope
Rust 的完整实现(runtime/queries/rust/locals.scm)展示了更细粒度的做法——除了函数与块,还把结构体、枚举、trait、impl、闭包、类型别名都声明为作用域,这样 self 或字段在各自的 item 内能被正确解析:
; Scopes
[
(function_item)
(struct_item)
(enum_item)
(union_item)
(type_item)
(trait_item)
(impl_item)
(closure_expression)
(block)
] @local.scope
Python 的对应实现(runtime/queries/python/locals.scm)则以模块级作为最外层作用域:
;; Scopes
[
(module)
(function_definition)
(lambda)
] @local.scope
编写时的经验法则是:作用域应取"变量生命周期终止"的边界。若语言有嵌套作用域(如函数内还有 lambda),把它们都标进来即可,匹配算法天然支持嵌套可见性(见下文"匹配机制")。
捕获二:@local.definition.* 定义与高亮类后缀
@local.definition.* 标记一个引入局部符号的名称节点。捕获名中 @local.definition. 之后的后缀(suffix),会成为解析到该定义的引用所继承的高亮类。例如 @local.definition.variable.parameter 会让所有匹配到它的引用以 variable.parameter 的样式渲染。
指南示例:
(function_item
(parameters
(parameter
pattern: (identifier) @local.definition.variable.parameter)))
常用后缀与 highlights.scm 中的高亮类一一对应:variable、variable.parameter、variable.builtin、variable.mutable、function、namespace、type、constant 等。从源码结构看,这些后缀最终经 helix-core/src/syntax.rs 中的 reconfigure_highlights(&config, &loader.scopes()) 与 loader 的作用域(scopes)体系对接,因此后缀应当使用主题里实际存在的高亮类名,否则覆写后的样式可能回退为默认前景色。
真实项目中的定义捕获通常分几类,以下均以 Rust 查询文件(runtime/queries/rust/locals.scm)为参照:
1. 函数参数(L17-L22):
(function_item
(parameters
(parameter
pattern: (identifier) @local.definition.variable.parameter)))
(closure_parameters (identifier) @local.definition.variable.parameter)
2. 可变变量(L24-L31)——借助 mutable_specifier 子节点区分 mut 绑定,这正是 variable.mutable 高亮在 Rust 中生效的来源:
(let_declaration
(mutable_specifier)
pattern: (identifier) @local.definition.variable.mutable)
3. 特殊内建标识符——Python 查询(runtime/queries/python/locals.scm)展示了用 #any-of? 谓词把 self/cls 标为内建变量的技巧:
(parameters
(identifier) @local.definition.variable.builtin
(#any-of? @local.definition.variable.builtin "self" "cls")) ; label self/cls as builtin
4. import 语句——把导入名定义为 namespace,这样后续对该模块名的引用会按命名空间着色(Python 查询 L36-L42):
(import_statement
name: (dotted_name
(identifier) @local.definition.namespace))
(aliased_import
alias: (identifier) @local.definition.namespace)
捕获三:@local.reference 引用
@local.reference 把标识符节点标记为"潜在的局部引用"。Helix 会沿着包围该节点的作用域链向内查找同名定义,若找到,引用点即被改按定义的高亮类渲染。
指南的最小写法:
(identifier) @local.reference
几乎所有语言查询都会用这一条"兜底"规则(如 runtime/queries/rust/locals.scm 中的 (identifier) @local.reference 与 (self) @local.reference)。宽泛的引用规则并不可怕,因为后续可以用 discard 精确排除不该参与解析的节点(见下一节)。
Discard 捕获:取消特定节点上的引用解析
locals.scm 中任何不是 @local.scope、@local.reference、@local.definition.* 的捕获都是 discard。它的作用是把某个节点从"可解析为引用"的集合中剔除,同时完全不影响 highlights.scm 对这些节点的着色。
典型场景:某些标识符在语法上看起来像变量引用,语义上却不是——比如关键字参数名、结构体字面量中的字段名。指南示例:
; Keyword argument names in a call are not variable references.
(keyword_argument
name: (identifier) @_)
这一模式在 Python 查询里被直接使用(runtime/queries/python/locals.scm):
; don't make the name of kwargs locals
(keyword_argument
name: (identifier) @_)
Rust 查询则用 discard 处理了更多边缘情况(runtime/queries/rust/locals.scm):生命周期与标签(L45-L47)、带作用域的函数调用名避免被误染成变量色(L49-L56)、函数定义名与枚举变体名(L58-L64)。
两条关键规则必须牢记:
- 优先级由文件中的位置决定:
locals.scm中靠后的 pattern 优先级更高,靠前的优先级更低。 - discard 必须写在它要覆盖的
@local.reference之后,否则兜底规则会"赢过"它,discard 形同虚设。这也是上述两个真实查询文件都把(identifier) @local.reference放在前面、各种@_排除放在后面的原因。
命名约定上,discard 捕获建议用下划线开头的名字(@_、@_keyword)以表达"丢弃"意图;但技术上任何非 @local.* 的名称都具备 discard 效果。
定义与引用的匹配机制
指南 How definitions and references are matched 一节明确了匹配算法的三个要点:
- 按文本比较:Helix 将引用节点的文本与当前作用域可见范围内各定义的文本逐一对比,名字相同即视为匹配。这是一种轻量、纯语法层的解析,不依赖语义分析或 LSP。
- 内层作用域遮蔽外层:若同名定义存在于多个嵌套层级,内层定义胜出(与主流语言的词法作用域语义一致)。
- 无匹配则回退基线高亮:找不到定义时,引用保留
highlights.scm赋予的原始高亮,行为与没有 locals 查询时完全相同。
"按文本比较"意味着该机制对重名但不同符号的情况(如两个同名函数在不同文件/模块)只保证词法作用域内的正确性——这是语法级方案的固有边界,也是它与 LSP 语义能力互补而非竞争的原因。
在仓库中定位与验证 locals 查询
对阅读或贡献该仓库的开发者,以下路径是最常用的切入点:
| 目标 | 路径 |
|---|---|
| 官方指南原文 | book/src/guides/locals.md |
查询编译入口(读取 locals.scm) |
helix-core/src/syntax.rs |
| 语言 id 与查询目录映射 | languages.toml |
| Rust 语言完整示例 | runtime/queries/rust/locals.scm |
Python 语言完整示例(含 #any-of? 用法) |
runtime/queries/python/locals.scm |
| 高亮查询编写指南(姊妹文档) | book/src/guides/highlights.md |
| 新增语言全流程(含查询文件放置规则) | book/src/guides/adding_languages.md |
highlights.md 与 locals.md 在 book/src/SUMMARY.md 中并列收录,前者负责"节点基础着色",后者负责"引用继承定义着色",两者配合才构成 Helix 的完整标识符着色方案。
小结
locals.scm是放在runtime/queries/{language}/下的可选查询文件,与highlights.scm并行编译、独立解析,仅在某次引用成功解析到定义时覆写高亮;- 三类核心捕获各司其职:
@local.scope划作用域,@local.definition.*声明定义并以后缀指定继承的高亮类,@local.reference标记候选引用; - 其余捕获均为 discard,用于精准排除伪引用,且必须写在被覆盖的
@local.reference之后才能生效; - 匹配基于"引用文本 vs 可见定义文本"的比较,内层遮蔽外层,无匹配则回退
highlights.scm基线高亮。
掌握以上要点后,为缺少 locals 支持的语言编写查询就是机械而清晰的工作:列出作用域节点、按语义给定义分类、写一条宽引用兜底,再用 discard 修剪边缘情况。
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