入门 Zed Industries 2026-09-14 17:42:20 · 0 阅读

第131章 Zed 语言扩展:元数据、语法解析与服务器配置详解

p> 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),ific 文本对象将默认使用这些。

如果你不确定 textobjects.scm 中该填什么,可以参考 nvim-treesitter-textobjectsHelix 编辑器,它们都为许多语言提供了查询。你还可以通过查看 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_typetoken_modifiers 的规则均会被应用。位于前面的规则优先级更高。若无规则匹配,该 Token 将不显示高亮效果。

规则按以下优先级顺序应用(由高到低):

  1. 用户设置 — 来自设置文件中 semantic_token_rules 的规则。
  2. 扩展规则 — 来自扩展语言目录中 semantic_token_rules.json 的规则。
  3. 默认规则 — 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"

评论 (0)