第131章 Zed 语言扩展:元数据、语法解析与服务器配置详解
- 语言元数据与配置
- 语法解析器(Grammar)
- 查询(Queries)
- 语言服务器
语言元数据
Zed 支持的每种语言,都必须在扩展内的 languages 目录下定义一个子目录。
该子目录需包含一个名为 config.toml 的文件,结构如下:
name = "My Language"
grammar = "my-language"
path_suffixes = ["myl"]
line_comments = ["# "]
name(必填):人类可读的语言名称,显示在“选择语言”下拉菜单中。grammar(必填):语法的名称。语法的注册是独立进行的,详见下文。path_suffixes:应与该语言关联的文件后缀数组。与设置中的file_types不同,此处不支持通配符模式。line_comments:用于标识行注释的字符串数组,配合editor::ToggleComments键位({#kb editor::ToggleComments})来切换代码行的注释状态。tab_size:定义该语言的缩进/制表符宽度(默认为4)。hard_tabs:是否使用制表符进行缩进(true),而非空格(false,默认值)。first_line_pattern:正则表达式,可与(上文提到的)path_suffixes或设置中的file_types结合,用于匹配应使用此语言的文件。例如,Zed 利用此功能通过匹配脚本首行的 Shebang 行 来识别 Shell 脚本。debuggers:用于标识该语言下调试器的字符串数组。启动调试器的New Process Modal时,Zed 会按照此数组中的条目顺序排列可用的调试器。
Grammar(语法)
Zed 使用 Tree-sitter 解析库来提供内置的语言专属功能。许多语言都有现成的语法可用,你也可以自己开发语法。Zed 越来越多的功能都是基于 Tree-sitter 查询对语法树做模式匹配来实现的。如上所述,扩展中定义的每种语言都必须指定用于解析的 Tree-sitter 语法名称。这些语法在扩展的 extension.toml 文件中单独注册,例如:
[grammars.gleam]
repository = "https://github.com/gleam-lang/tree-sitter-gleam"
rev = "58b7cac8fc14c92b0677c542610d8738c373fa81"
repository 字段必须指定加载 Tree-sitter 语法的仓库地址,rev 字段必须填写要使用的 Git 版本,比如某次 Git 提交的 SHA。如果你在本地开发扩展,想从本地文件系统加载语法,可以在 repository 中使用 file:// URL。一个扩展可以通过引用多个 tree-sitter 仓库来提供多个语法。
Tree-sitter Queries(Tree-sitter 查询)
Zed 利用 Tree-sitter 查询语言生成的语法树来实现以下功能:
- 语法高亮
- 括号匹配
- 代码大纲/结构
- 自动缩进
- 代码注入
- 语法覆盖
- 文本遮蔽
- 可运行代码检测
- 选中类、函数等
以下章节以 JSON 语法为例,详细说明 Tree-sitter 查询如何在 Zed 中实现这些功能。
语法高亮
在 Tree-sitter 中,highlights.scm 文件定义了针对特定语法的语法高亮规则。
以下是 JSON 的 highlights.scm 中的一个示例:
(string) @string
(pair
key: (string) @property.json_key)
(number) @number
该查询标记了字符串、对象键和数值以应用高亮。以下是主题支持的完整捕获列表:
| 捕获 | 描述 |
|---|---|
| @attribute | 捕获属性 |
| @boolean | 捕获布尔值 |
| @comment | 捕获注释 |
| @comment.doc | 捕获文档注释 |
| @constant | 捕获常量 |
| @constant.builtin | 捕获内置常量 |
| @constructor | 捕获构造函数 |
| @embedded | 捕获嵌入式内容 |
| @emphasis | 捕获强调文本 |
| @emphasis.strong | 捕获强烈强调文本 |
| @enum | 捕获枚举 |
| @function | 捕获函数 |
| @hint | 捕获提示 |
| @keyword | 捕获关键字 |
| @label | 捕获标签 |
| @link_text | 捕获链接文本 |
| @link_uri | 捕获链接 URI |
| @number | 捕获数值 |
| @operator | 捕获运算符 |
| @predictive | 捕获预测文本 |
| @preproc | 捕获预处理器指令 |
| @primary | 捕获主要元素 |
| @property | 捕获属性 |
| @punctuation | 捕获标点符号 |
| @punctuation.bracket | 捕获括号 |
| @punctuation.delimiter | 捕获分隔符 |
| @punctuation.list_marker | 捕获列表标记 |
| @punctuation.special | 捕获特殊标点符号 |
| @string | 捕获字符串字面量 |
| @string.escape | 捕获字符串中的转义字符 |
| @string.regex | 捕获正则表达式 |
| @string.special | 捕获特殊字符串 |
| @string.special.symbol | 捕获特殊符号 |
| @tag | 捕获标签 |
| @tag.doctype | 捕获文档类型声明(如 HTML) |
| @text.literal | 捕获字面文本 |
| @title | 捕获标题 |
| @type | 捕获类型 |
| @type.builtin | 捕获内置类型 |
| @variable | 捕获变量 |
| @variable.special | 捕获特殊变量 |
| @variable.parameter | 捕获函数或方法参数 |
| @variant | 捕获变体 |
回退捕获
单个 Tree-sitter 模式可以在同一节点上指定多个捕获,以定义回退高亮。 Zed 从右向左解析它们:首先尝试最右侧的捕获,如果当前主题没有对应的样式,则回退到左侧的下一个捕获,依此类推。
示例如下:
(type_identifier) @type @variable
在此例中,Zed 首先尝试从主题解析 @variable。如果主题为 @variable 定义了样式,则使用该样式;否则,Zed 回退到 @type。
这在语言希望提供首选高亮(可能并非所有主题都支持),同时仍回退到大多数主题都定义的更常见捕获时很有用。
括号匹配
brackets.scm 文件定义匹配的括号。
以下是 JSON 的 brackets.scm 文件中的一个示例:
("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)
此查询识别开启和闭合的括号、花括号及引号。
| 捕获 | 描述 |
|---|---|
| @open | 捕获左括号、左花括号和左引号 |
| @close | 捕获右括号、右花括号和右引号 |
Zed 用这些捕获来高亮匹配的括号:为每对括号涂上不同颜色(即"彩虹括号"),并在光标位于括号对内时高亮这对括号。
如果想关闭彩虹括号着色,可在对应的 brackets.scm 条目中添加如下内容:
(("\"" @open "\"" @close) (#set! rainbow.exclude))
代码大纲/结构
outline.scm 文件定义代码大纲的结构。
下面是 JSON 的 outline.scm 文件示例:
(pair
key: (string (string_content) @name)) @item
这个查询会捕获对象的键,用于构建大纲结构。
| 捕获 | 描述 |
|---|---|
| @name | 捕获对象键的内容 |
| @item | 捕获整个键值对 |
| @context | 捕获为大纲条目提供上下文的元素 |
| @context.extra | 捕获大纲条目的额外上下文信息 |
| @annotation | 捕获注解大纲条目的节点(文档注释、属性、装饰器)[^1] |
[^1]: 这些注解会被 Assistant 在生成代码修改步骤时使用。
自动缩进
indents.scm 文件定义缩进规则。
基于语法的缩进与反缩进
| 捕获 | 描述 |
|---|---|
| @indent | 用捕获的节点定义一个缩进范围 |
| @start | 将 @indent 范围的起点移动到所捕获节点的末尾 |
| @end | 将 @indent 范围的终点移动到所捕获节点的开头 |
| @outdent | 让最内层缩进范围在所捕获节点的开头处结束 |
例如,要对 if_statement 节点的全部内容进行缩进:
(if_statement) @indent
缩进范围从节点开始处延伸至结束处。
HTML 元素包含其开标签和闭标签。若只想对中间内容进行缩进:
(element
(start_tag) @start ; 从开标签后开始缩进
(end_tag)? @end) @indent ; 在闭标签前结束缩进
后续的 case 标签相对于前一个 case 语句体必须向右退格,以便使同级标签对齐:
(compound_statement
(case_statement
":" @start) ; 开始缩进 case 语句体
"}" @end) @indent
(compound_statement
(case_statement)
(case_statement) @outdent) ; 对齐后续的 case 标签
基于行模式的缩进与退格
对于基于行内容而非语法节点的缩进规则,使用 config.toml 中的以下选项:
| 选项 | 描述 |
|---|---|
increase_indent_pattern |
匹配行后的下一行增加一级缩进 |
decrease_indent_pattern |
匹配行减少一级缩进,不依赖语法上下文 |
decrease_indent_patterns |
匹配行与前方允许的语法结构对齐 |
例如,对以 : 结尾的行进行缩进:
increase_indent_pattern = ":\\s*$"
例如,对以 end 开头的行进行退格:
decrease_indent_pattern = "^\\s*end\\b"
子句与关联代码块的对齐
当某一行应与关联代码块对齐而非单纯向左移动一级时,使用 decrease_indent_patterns。在 indents.scm 中,用带名称的 @start.<name> 捕获标记代码块的起始位置:
(if_statement) @start.if
在 config.toml 中,将捕获后缀列于 valid_after 中:
decrease_indent_patterns = [
{ pattern = "^\\s*else\\b", valid_after = ["if"] },
]
以 else 开头的行与同级或更低缩进级别下最近的一个 @start.if 对齐。如果 Zed 找不到匹配的块,则保持缩进不变。
像 @start.if 这样的命名捕获用于标记这些规则的作用块。与 @start 不同,它们不会改变 @indent 的范围。
Zed 按顺序检查规则,并在遇到第一个匹配的 pattern 后停止。应将更具体的模式放在重叠的通用模式之前。
代码注入
injections.scm 文件定义了将一种语言嵌入另一种语言的规则,例如 Markdown 中的代码块或 Python 字符串中的 SQL 查询。
下面是 Markdown 的 injections.scm 文件中的一个示例:
(fenced_code_block
(info_string
(language) @injection.language)
(code_fence_content) @injection.content)
((inline) @content
(#set! injection.language "markdown-inline"))
该查询识别围栏代码块,捕获信息字符串中指定的语言以及块内的内容。它还捕获内联内容并将其语言设置为 "markdown-inline"。
| 捕获 | 描述 |
|---|---|
| @injection.language | 捕获代码块的语言标识符 |
| @injection.content | 捕获需视为另一种语言的内容 |
注意,由于 JSON 不支持语言注入,因此这里不能用它作为示例。
语法覆盖
overrides.scm 文件定义了可用于在特定语言结构中覆盖某些编辑器设置的语法 作用域。
例如,有一个名为 word_characters 的语言特定设置,它控制哪些非字母字符被视为单词的一部分,比如在双击选择变量时。在 JavaScript 中,"$" 和 "#" 被视为单词字符。
还有一个语言专属的设置项 completion_query_characters,用来控制哪些字符会触发自动补全建议。以 JavaScript 为例,当光标位于字符串(string)内时,- 应当被视为补全触发字符。为此,JavaScript 的 overrides.scm 文件包含如下模式:
[
(string)
(template_string)
] @string
同时 JavaScript 的 config.toml 中包含这个设置:
word_characters = ["#", "$"]
[overrides.string]
completion_query_characters = ["-"]
你还可以在特定作用域中禁用某些自动闭合的括号。例如,要禁止在字符串内自动闭合 ',可以在 JavaScript 的 config.toml 中这样写:
brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
# 其他括号对...
]
Range inclusivity(范围的包含性)
默认情况下,overrides.scm 中定义的范围是不包含边界(exclusive)的。也就是说,在上面的例子里,如果光标位于字符串引号之外,string 作用域就不会生效。有时你可能希望范围包含边界(inclusive),只需在查询的捕获名后加上 .inclusive 后缀即可。
例如在 JavaScript 中,我们还要在注释内禁用单引号自动闭合,而且注释作用域必须延伸到行注释之后的换行符为止。为此,JavaScript 的 overrides.scm 包含以下模式:
(comment) @comment.inclusive
Text objects(文本对象)
textobjects.scm 文件定义了按文本对象导航的规则。该功能在 Zed v0.165 中引入,目前仅用于 Vim 模式。
Vim 提供了两个粒度级别的文件导航方式:用 [] 等按键按章节跳转,用 ]m 等按键按方法跳转。即使是不支持函数和类的语言,也可以通过定义类似的概念来良好适配。比如 CSS 中,规则集(rule-set)被定义为方法,媒体查询(media-query)被定义为类。
对于支持闭包的语言,这些在 Zed 中通常不应被计为函数。不过这是尽力而为的,因为像 JavaScript 这样的语言在语法上并不区分闭包和顶层函数声明。
对于 C 等具有声明的语言,请提供匹配 @class.around 或 @function.around 的查询。如果没有内部对象(inside),if 和 ic 文本对象将默认使用这些。
如果你不确定 textobjects.scm 中该填什么,可以参考 nvim-treesitter-textobjects 和 Helix 编辑器,它们都为许多语言提供了查询。你还可以通过查看 Zed 的 内置语言 来了解如何适配这些查询。
| 捕获 | 描述 | Vim 模式 |
|---|---|---|
| @function.around | 整个函数定义或文件中等效的小节。 | [m、]m、[M、]M 移动操作。af 文本对象 |
| @function.inside | 函数体(花括号内的内容)。 | if 文本对象 |
| @class.around | 整个类定义或文件中等效的大节。 | [[、]]、[]、][ 移动操作。ac 文本对象 |
| @class.inside | 类定义的内容。 | ic 文本对象 |
| @comment.around | 整个注释(例如所有相邻的行注释,或块注释)。 | gc 文本对象 |
| @comment.inside | 注释的内容。 | igc 文本对象(很少被支持) |
示例:
; 仅在函数中捕获方法的内容
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
; 匹配无函数体的声明中的 function.around
(function_signature_item) @function.around
; 将所有相邻注释合并为一个
(comment)+ @comment.around
文本删减
redactions.scm 文件用于定义文本遮蔽规则。在协作或共享屏幕时,它确保特定的语法节点以遮蔽模式渲染,从而防止敏感信息泄露。
以下是 JSON 语言 redactions.scm 文件的一个示例:
(pair value: (number) @redact)
(pair value: (string) @redact)
(array (number) @redact)
(array (string) @redact)
该查询将键值对和数组中的数值及字符串值标记为需要遮蔽。
| 捕获变量 | 说明 |
|---|---|
| @redact | 捕获待遮蔽的值 |
可运行代码检测
runnables.scm 文件定义检测可运行代码的规则。
以下是 JSON 语言 runnables.scm 文件的一个示例:
(
(document
(object
(pair
key: (string
(string_content) @_name
(#eq? @_name "scripts")
)
value: (object
(pair
key: (string (string_content) @run @script)
)
)
)
)
)
(#set! tag package-script)
(#set! tag composer-script)
)
该查询用于检测 package.json 和 composer.json 文件中的可运行脚本。
@run 捕获变量指定了在编辑器中显示“运行”按钮的位置。除了以 underscores 前缀开头的捕获变量外,其他捕获变量在运行代码时会以 ZED_CUSTOM_$(capture_name) 为前缀,作为环境变量暴露。
| 捕获变量 | 说明 |
|---|---|
| @_name | 捕获 "scripts" 键 |
| @run | 捕获脚本名称 |
| @script | 同样捕获脚本名称(用于不同目的) |
语言服务器
Zed 使用 Language Server Protocol 提供高级语言支持。
扩展可以提供任意数量的 language server。要让扩展提供 language server,需在 extension.toml 中添加一个条目,写明 language server 的名称及其适用的语言。languages 列表中的条目必须与该语言 config.toml 文件中的 name 字段一致:
[language_servers.my-language-server]
name = "My Language LSP"
languages = ["My Language"]
然后在扩展的 Rust 代码中,实现扩展的 language_server_command 方法:
impl zed::Extension for MyExtension {
fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
Ok(zed::Command {
command: get_path_to_language_server_executable()?,
args: get_args_for_language_server()?,
env: get_env_for_language_server()?,
})
}
}
你还可以通过 Extension trait 中的若干可选方法来自定义 language server 的处理方式。例如,用 label_for_completion 方法控制补全项的显示样式。完整的方法列表请参阅 Zed 扩展 API 文档。
基于 Semantic Tokens 的语法高亮
Zed 支持利用所连接 language server 提供的 semantic tokens 进行语法高亮。该功能目前默认关闭,但可以在设置文件中启用:
{
// 全局启用 semantic tokens,并与各语言的 tree-sitter 高亮叠加:
"semantic_tokens": "combined",
// 或者按语言单独设置:
"languages": {
"Rust": {
// 不用 tree-sitter,仅使用 LSP semantic tokens:
"semantic_tokens": "full"
}
}
}
semantic_tokens 设置接受以下取值:
"off"(默认):不向 language server 请求 semantic tokens。"combined":将 LSP semantic tokens 与 tree-sitter 高亮结合使用。"full":仅使用 LSP 语义 token,替代 tree-sitter 高亮。
扩展提供的语义 token 规则
语言扩展可以为其语言服务器的自定义 token 类型提供默认的语义 token 规则。为此,请在语言目录下与 config.toml 同级放置一个 semantic_token_rules.json 文件:
my-extension/
languages/
my-language/
config.toml
highlights.scm
semantic_token_rules.json
该文件使用与用户设置中 semantic_token_rules 数组相同的格式——即规则对象的 JSON 数组:
[
{
"token_type": "lifetime",
"style": ["lifetime"]
},
{
"token_type": "builtinType",
"style": ["type"]
},
{
"token_type": "selfKeyword",
"style": ["variable.special"]
}
]
当语言服务器上报 Zed 内置默认规则未涵盖的自定义(非标准)语义 token 类型时,此功能非常有用。扩展提供的规则作为该语言的合理默认值——用户始终可以通过设置文件中的 semantic_token_rules 覆盖它们,而内置默认规则仅在用户和扩展规则都未匹配时才使用。
自定义语义 token 样式
Zed 支持自定义语义 token 使用的样式。您可以在设置文件中定义规则,以定制语义 token 在主题中映射到样式的方式。
{
"global_lsp_settings": {
"semantic_token_rules": [
{
// 将宏高亮为关键字。
"token_type": "macro",
"style": ["syntax.keyword"]
},
{
// 以粗体红色高亮未解析的引用。
"token_type": "unresolvedReference",
"foreground_color": "#c93f3f",
"font_weight": "bold"
},
{
// 为所有可变变量/引用等添加下划线。
"token_modifiers": ["mutable"],
"underline": true
}
]
}
}
所有匹配指定 token_type 和 token_modifiers 的规则均会被应用。位于前面的规则优先级更高。若无规则匹配,该 Token 将不显示高亮效果。
规则按以下优先级顺序应用(由高到低):
- 用户设置 — 来自设置文件中
semantic_token_rules的规则。 - 扩展规则 — 来自扩展语言目录中
semantic_token_rules.json的规则。 - 默认规则 — Zed 针对标准 LSP Token 类型内置的规则。
semantic_token_rules 数组中每条规则的定义如下:
token_type:根据 LSP 规范 定义的语义 Token 类型。若省略,该规则匹配所有 Token 类型。token_modifiers:需匹配的语义 Token 修饰符列表。所有修饰符均存在时才算匹配成功。style:需使用的当前语法主题样式列表。将使用找到的第一个样式,下方的任何设置项都会覆盖该样式。foreground_color:用于该 Token 类型的前景色,格式为十六进制(例如"#ff0000")。background_color:用于该 Token 类型的背景色,格式为十六进制(例如"#ff0000")。underline:布尔值或下划线颜色(十六进制格式)。若为true,则使用文本颜色添加下划线。strikethrough:布尔值或删除线颜色(十六进制格式)。若为true,则使用文本颜色添加删除线。font_weight:取值范围为"normal"或"bold"。font_style:取值范围为"normal"或"italic"。
多语言支持
如果你的语言服务器支持其他语言,可以使用 language_ids 将 Zed 的 languages 映射到所需的 LSP 特定 languageId 标识符:
[language-servers.my-language-server]
name = "Whatever LSP"
languages = ["JavaScript", "HTML", "CSS"]
[language-servers.my-language-server.language_ids]
"JavaScript" = "javascript"
"TSX" = "typescriptreact"
"HTML" = "html"
"CSS" = "css"