首页
/ Helix 的 locals.scm 查询:用作用域解析让局部变量高亮区别于全局变量

Helix 的 locals.scm 查询:用作用域解析让局部变量高亮区别于全局变量

2026-09-05 23:11:00作者:邵娇湘

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 中的高亮类一一对应:variablevariable.parametervariable.builtinvariable.mutablefunctionnamespacetypeconstant 等。从源码结构看,这些后缀最终经 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)。

两条关键规则必须牢记:

  1. 优先级由文件中的位置决定locals.scm靠后的 pattern 优先级更高,靠前的优先级更低。
  2. discard 必须写在它要覆盖的 @local.reference 之后,否则兜底规则会"赢过"它,discard 形同虚设。这也是上述两个真实查询文件都把 (identifier) @local.reference 放在前面、各种 @_ 排除放在后面的原因。

命名约定上,discard 捕获建议用下划线开头的名字(@_@_keyword)以表达"丢弃"意图;但技术上任何非 @local.* 的名称都具备 discard 效果。

定义与引用的匹配机制

指南 How definitions and references are matched 一节明确了匹配算法的三个要点:

  1. 按文本比较:Helix 将引用节点的文本与当前作用域可见范围内各定义的文本逐一对比,名字相同即视为匹配。这是一种轻量、纯语法层的解析,不依赖语义分析或 LSP。
  2. 内层作用域遮蔽外层:若同名定义存在于多个嵌套层级,内层定义胜出(与主流语言的词法作用域语义一致)。
  3. 无匹配则回退基线高亮:找不到定义时,引用保留 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.mdlocals.mdbook/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 修剪边缘情况。

登录后查看全文
热门项目推荐
相关项目推荐