Zed 语言扩展开发指南:从 config.toml 到 Tree-sitter 查询与 LSP 集成
【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed
Zed 是一套开放的语言基础设施:编辑器自身内置的语言支持与第三方扩展语言包共享同一套“语言元数据 + Tree-sitter 语法 + LSP 服务器”三层架构。本文以 Zed 仓库中官方文档 languages.md 为骨架,结合仓库内真实扩展(HTML、GLSL、Test Extension)讲解如何为一种新语言编写config.toml、注册 Grammar、编写各类 Tree-sitter.scm查询,以及如何接入语言服务器与语义化 Token(Semantic Tokens)。读完你可以独立为 Zed 打造一个具备语法高亮、缩进、大纲、括号匹配与 LSP 补全能力的完整语言扩展。
语言支持的四个组成部分
Zed 中一种语言的支持由四层组成,扩展开发需要逐一提供或配置:
- 语言元数据与配置(Language metadata and configuration)——即
config.toml,声明语言名称、语法、文件后缀、注释风格、缩进等信息; - 语法(Grammar)——基于 Tree-sitter 解析库的语法定义,在
extension.toml中单独注册; - 查询(Queries)——一系列
.scm(Tree-sitter Query)文件,用于在语法树上实现高亮、缩进、大纲等功能; - 语言服务器(Language servers)——通过 LSP 协议接入第三方服务器,提供补全、跳转等高级能力。
语言元数据:languages/<name>/config.toml
每种 Zed 支持的语言必须在扩展的languages目录下拥有一个子目录,子目录内必须包含名为config.toml的文件,其基本结构如下:
name = "My Language" grammar = "my-language" path_suffixes = ["myl"] line_comments = ["# "]字段含义如下:
name(必填):人类可读的名称,会显示在语言选择(Select Language)下拉框中。grammar(必填):Grammar 名称。Grammar 是单独注册的(详见下文),此字段只是引用其名字。path_suffixes:与该语言关联的文件后缀数组。与 settings 中的file_types不同,这里不支持 glob 模式,只能写具体后缀。line_comments:标识该语言行注释的字符串数组。它服务于editor::ToggleComments键位绑定,用于切换整行注释。tab_size:缩进/制表符尺寸,默认4。hard_tabs:是否用制表符缩进,true表示 Tab,默认false(空格)。first_line_pattern:一个正则表达式,可与path_suffixes(或 settings 中的file_types)配合,按文件首行内容匹配语言。Zed 就用它通过首行的 shebang 行来识别 Shell 脚本。debuggers:标识该语言可用调试器的字符串数组。在调试器的“新建进程(New Process Modal)”弹窗中,Zed 会按照该数组的顺序排列可用调试器。
真实示例:HTML 扩展的 config.toml
仓库自带的 HTML 扩展配置 展示了上述字段之外的更多可配置项,是学习config.toml的最佳参照:
name = "HTML" grammar = "html" path_suffixes = ["html", "htm", "shtml"] autoclose_before = ">})" block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 0 } wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "</", end_suffix = ">" } brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, ] completion_query_characters = ["-"] prettier_parser_name = "html" [overrides.default] linked_edit_characters = ["-"]从该配置可以归纳出文档正文未展开、但同样可用的键(这些键同样以内置语言为参考目标,官方在 languages.md 中以注释形式列出待补文档):autoclose_before、brackets(元素含start/end/close/newline/not_in,not_in可限定不出现在如comment、string等作用域内)、block_comment(多行注释的起止与前缀)、wrap_characters(成对包裹字符)、completion_query_characters、prettier_parser_name、code_fence_block_name、word_characters、collapsed_placeholder、auto_indent_on_paste、auto_indent_using_last_non_empty_line以及[overrides.<scope>]作用域级覆盖(如 HTML 在[overrides.default]下配置linked_edit_characters)。
注册 Tree-sitter Grammar:extension.toml 中的[grammars.*]
Zed 使用 Tree-sitter 解析库提供内置的语言级特性。许多语言都有现成语法,也可以自行开发语法(Tree-sitter 官方写作指南提供了从零编写语法的路径)。前面提到,扩展中定义的每种语言必须指定用于解析的 Grammar 名称,而这些 Grammar 要在扩展根目录的extension.toml中单独注册,例如:
[grammars.gleam] repository = "https://github.com/gleam-lang/tree-sitter-gleam" rev = "58b7cac8fc14c92b0677c542610d8738c373fa81"repository:指定加载 Grammar 的仓库地址;rev:要使用的 Git 修订号,例如某个 Git 提交的 SHA。
正在本地开发扩展时,若想从本机文件系统加载 Grammar,可将repository写成file://URL。一个扩展可以引用多个 Tree-sitter 仓库,从而提供多种 Grammar。
需要说明的是:本仓库的官方示例使用了rev键,而仓库内实际的 HTML 与 GLSL 扩展在 extension.toml、GLSL extension.toml 中填写的是commit字段(同样是提交 SHA),两种字段名均指向同一“Git 修订号”语义,书写时可参照官方文档使用rev。
Tree-sitter Queries:驱动编辑器核心功能的查询文件
Zed 借助 Tree-sitter 查询语言,在语法树上实现多项功能:
- 语法高亮(Syntax highlighting)
- 括号匹配(Bracket matching)
- 代码大纲/结构(Code outline/structure)
- 自动缩进(Auto-indentation)
- 代码注入(Code injections)
- 语法作用域覆盖(Syntax overrides)
- 文本脱敏(Text redactions)
- 可运行代码检测(Runnable code detection)
- 选择类/函数等代码块(Selecting classes, functions, etc.)
下文以 JSON 语法为例逐一展开每种查询文件的写法。
语法高亮:highlights.scm
Tree-sitter 中,highlights.scm文件定义某种语法的着色规则。JSON 的示例:
(string) @string (pair key: (string) @property.json_key) (number) @number该查询分别标记了字符串、对象键与数字用于高亮。主题支持的完整 capture 列表如下:
| Capture | 描述 |
|---|---|
| @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 | 捕获特殊符号(如 Ruby symbol) |
| @tag | 捕获标签(如 HTML 标签) |
| @tag.doctype | 捕获文档类型声明(如 HTML doctype) |
| @text.literal | 捕获字面文本 |
| @title | 捕获标题 |
| @type | 捕获类型 |
| @type.builtin | 捕获内置类型 |
| @variable | 捕获变量 |
| @variable.special | 捕获特殊变量 |
| @variable.parameter | 捕获函数/方法参数 |
| @variant | 捕获变体 |
Fallback captures:同节点的回退高亮
单个 Tree-sitter 模式可以在同一节点上指定多个 capture 以实现回退高亮。Zed从右向左解析:先尝试最右侧的 capture,若当前主题没有它的样式,则回退到左侧下一个 capture,依此类推。例如:
(type_identifier) @type @variable这里 Zed 会先从主题解析@variable:若主题为@variable定义了样式则使用它,否则回退到@type。当某语言希望提供一个并非所有主题都支持的首选高亮、同时又想回退到多数主题都有的通用 capture 时,这种写法非常有用。
括号匹配:brackets.scm
brackets.scm定义可配对的括号。JSON 的示例:
("[" @open "]" @close) ("{" @open "}" @close) ("\"" @open "\"" @close)| Capture | 描述 |
|---|---|
| @open | 捕获开括号、开大括号与引号 |
| @close | 捕获闭括号、闭大括号与引号 |
Zed 利用这些规则实现匹配括号高亮:为每对括号渲染不同颜色(“彩虹括号”),并在光标位于括号对内部时高亮它们。若要关闭某个条目的彩虹括号着色,可在对应brackets.scm条目中追加:
(("\"" @open "\"" @close) (#set! rainbow.exclude))仓库中 HTML 的 brackets.scm 即按同样思路为 HTML 标签与引号定义了@open/@close捕获。
代码大纲/结构:outline.scm
outline.scm定义代码大纲的结构。JSON 的示例捕获对象键生成大纲条目:
(pair key: (string (string_content) @name)) @item| Capture | 描述 |
|---|---|
| @name | 捕获对象键的内容(即大纲条目显示名) |
| @item | 捕获整个键值对(即大纲条目本体) |
| @context | 捕获为大纲条目提供上下文的元素 |
| @context.extra | 捕获大纲条目的额外上下文信息 |
| @annotation | 捕获注解大纲条目的节点(文档注释、属性、装饰器等)1 |
自动缩进:indents.scm与基于行模式的缩进规则
indents.scm定义缩进规则,分两种实现路径。
基于语法节点的缩进与反缩进
| Capture | 描述 |
|---|---|
| @indent | 用捕获的节点定义一个缩进范围 |
| @start | 将某个@indent范围的起点移动到所捕获节点的末尾 |
| @end | 将某个@indent范围的终点移动到所捕获节点的开头 |
| @outdent | 在捕获节点开始处结束最内层的缩进范围 |
例如缩进整个if_statement节点内容:
(if_statement) @indent缩进范围从节点起点延伸到终点。
HTML 元素包含开闭标签,若只想缩进二者之间的内容:
(element (start_tag) @start ; 在开标签之后开始缩进 (end_tag)? @end) @indent ; 在闭标签之前结束缩进当else/case后续标签需要与前一个分支对齐(而不是缩进到更深一层)时,可以让其从所在 case 主体反缩进:
(compound_statement (case_statement ":" @start) ; 从 case 主体开始缩进 "}" @end) @indent (compound_statement (case_statement) (case_statement) @outdent) ; 使后续 case 标签对齐仓库内 HTML 的 indents.scm 正是用@indent/@start/@end组合控制开标签到闭标签之间内容的缩进。
基于行模式的缩进与反缩进
当需要按“行内容”而非语法节点匹配时,可在config.toml中配置以下选项:
| Option | 描述 |
|---|---|
increase_indent_pattern | 匹配的行令其下一行缩进一级 |
decrease_indent_pattern | 匹配的行反缩进一级,不考虑语法上下文 |
decrease_indent_patterns | 匹配的行与某个允许的更早语法结构对齐 |
例如“行尾以:结尾则下一行缩进”:
increase_indent_pattern = ":\\s*$"例如“以end开头的行反缩进”:
decrease_indent_pattern = "^\\s*end\\b"让子句与相关代码块对齐:decrease_indent_patterns+valid_after
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对齐;若找不到匹配的代码块,缩进保持不变。注意:@start.if这类命名捕获只用于标记代码块,与@start不同,它们不会改变@indent的范围。Zed 会按顺序检查规则,命中第一条pattern后即停止,因此较具体的模式应写在较通用的模式之前。
代码注入:injections.scm
injections.scm定义一种语言嵌入另一种语言的规则,例如 Markdown 中的代码块、Python 字符串中的 SQL。Markdown 的示例:
(fenced_code_block (info_string (language) @injection.language) (code_fence_content) @injection.content) ((inline) @content (#set! injection.language "markdown-inline"))该查询识别围栏代码块:捕获 info string 中声明的语言标识与块内内容,并把它们交给对应语言解析;同时捕获行内内容并注入为markdown-inline语言。
| Capture | 描述 |
|---|---|
| @injection.language | 捕获代码块的语言标识 |
| @injection.content | 捕获需要按另一种语言处理的内容 |
注意 JSON 不支持语言注入,因此这里不能再用 JSON 举例。仓库内 GLSL 扩展 也提供了类似机制。
语法作用域覆盖:overrides.scm+[overrides.*]
overrides.scm定义语法作用域(scopes),用于在特定语言结构中覆盖某些编辑器设置。例如语言级设置word_characters控制哪些非字母字符被视为单词的一部分(双击选中变量时生效),JavaScript 中$与#是单词字符;另一项语言级设置completion_query_characters控制哪些字符会触发自动补全。当光标位于字符串内时,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"] }, # other pairs... ]作用域范围的包含性
默认情况下overrides.scm定义的范围是排他(exclusive)的:仍以上例而言,若光标位于界定字符串的引号之外,string作用域不会生效。有时需要让范围变成包含(inclusive),做法是在查询的 capture 名上加.inclusive后缀。例如 JavaScript 需要在注释中也禁用单引号自动闭合,且注释作用域要延伸到行注释后的换行处,于是其overrides.scm为:
(comment) @comment.inclusive文本对象:textobjects.scm
textobjects.scm定义按文本对象导航的规则,于 Zed v0.165 加入,目前仅在 Vim 模式下使用。Vim 提供两种文件内导航粒度:用[]等按键的逐“段”移动,以及用]m等的逐“方法”移动。即使语言本身没有函数与类的概念,也可以通过映射获得良好效果:例如 CSS 把一个 rule-set 视为“方法”、把 media-query 视为“类”。含闭包的语言通常不应把闭包当作函数——但这属于尽力而为,因为 JavaScript 之类的语言在语法上并不区分闭包与顶层函数声明。对 C 这类以声明为主的语言,需要提供匹配@class.around或@function.around的查询;在没有 inside 捕获时,if/ic文本对象会默认回退到它们。
若不确定textobjects.scm该写什么,可以参考 nvim-treesitter-textobjects 与 Helix 编辑器为多种语言提供的查询,再对照 Zed 内置语言(本仓库的 crates/languages/src)来适配。
| Capture | 描述 | Vim 模式 |
|---|---|---|
| @function.around | 整个函数定义,或文件中等价的一小段 | [m、]m、[M、]M移动;af文本对象 |
| @function.inside | 函数体(花括号内部的内容) | if文本对象 |
| @class.around | 整个类定义,或文件中等价的较大片段 | [[、]]、[]、][移动;ac文本对象 |
| @class.inside | 类定义的内容 | ic文本对象 |
| @comment.around | 整段注释(如所有相邻行注释或一个块注释) | gc文本对象 |
| @comment.inside | 注释的内容 | igc文本对象(较少支持) |
示例:
; 只把方法体内含纳入 function (method_definition body: (_ "{" (_)* @function.inside "}")) @function.around ; 为没有函数体的声明匹配 function.around (function_signature_item) @function.around ; 把所有相邻注释合并为一段 (comment)+ @comment.around文本脱敏:redactions.scm
redactions.scm定义文本脱敏规则。协作与共享屏幕时,Zed 会以脱敏模式渲染某些语法节点,避免泄露敏感数据。JSON 的示例:
(pair value: (number) @redact) (pair value: (string) @redact) (array (number) @redact) (array (string) @redact)| Capture | 描述 |
|---|---|
| @redact | 捕获需脱敏的值 |
该查询将键值对与数组中的数值、字符串标记为脱敏对象。
可运行代码检测:runnables.scm
runnables.scm定义可运行代码的检测规则。以下 JSON 示例可在 package.json 与 composer.json 中检测到可运行脚本:
( (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) )@run捕获指定运行按钮应出现在编辑器的哪个位置。其余捕获(下划线_前缀的除外)在运行代码时会以ZED_CUSTOM_$(capture_name)前缀的环境变量形式暴露出来。
| Capture | 描述 |
|---|---|
| @_name | 捕获 "scripts" 键 |
| @run | 捕获脚本名(确定运行按钮位置) |
| @script | 同样捕获脚本名(供不同用途使用) |
Language Servers:接入 LSP
Zed 通过语言服务器协议(LSP)提供更高级的语言支持。一个扩展可以提供任意数量的语言服务器。
声明语言服务器并实现启动命令
在extension.toml中加入语言服务器条目,写明服务器名称及其适用的语言。languages列表中的条目必须与该语言config.toml里的name字段完全一致:
[language_servers.my-language-server] name = "My Language LSP" languages = ["My Language"]然后在扩展的 Rust 代码中实现Extensiontrait 的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()?, }) } }返回的zed::Command由command(可执行文件路径)、args(参数)与env(环境变量)组成。本仓库 Test Extension 源码 给出了贴合实际的实现:先按平台选择二进制路径并下载安装语言服务器,再通过language_server_command返回带args: vec!["lsp".to_string()]的启动命令,并在 language_servers 声明 中对应注册。其 GLSL 扩展 的language_servers声明则展示了languages数组的写法。
你还可以用Extensiontrait 上的多个可选方法自定义对语言服务器的处理,例如用label_for_completion定制补全项的样式(Test Extension 中即通过该方法把 Gleam 的类型签名渲染成 "let a: …" 风格的补全标签)。完整方法列表见 Zed 扩展 API 文档(docs.rs 的 zed_extension_api)。
基于语义 Token 的语法高亮(Semantic Tokens)
Zed 支持使用语言服务器上报的语义 Token 进行语法高亮。该特性默认关闭,可在设置文件中启用:
{ // 全局启用语义 Token,并叠加 tree-sitter 高亮 "semantic_tokens": "combined", // 或者按语言单独指定: "languages": { "Rust": { // 不使用 tree-sitter,只用 LSP 语义 Token: "semantic_tokens": "full" } } }semantic_tokens设置取值:
"off"(默认):不向语言服务器请求语义 Token;"combined":将 LSP 语义 Token 与 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"] } ]当语言服务器上报自定义的(非标准)Token 类型、而 Zed 内置默认规则未覆盖时,这套机制尤其有用。扩展提供的规则作为该语言的合理默认值——用户永远可以在自己的设置中通过semantic_token_rules覆盖它们;只有用户与扩展规则都不匹配时,才会使用内置默认规则(内置默认规则文件见 assets/settings/default_semantic_token_rules.json)。
定制语义 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 不高亮。规则按如下优先级生效(从高到低):
- 用户设置——settings 文件中
semantic_token_rules的规则; - 扩展规则——扩展语言目录中
semantic_token_rules.json的规则; - 默认规则——Zed 针对标准 LSP Token 类型内置的规则。
semantic_token_rules数组中每条规则的字段定义如下:
token_type:LSP 规范定义的语义 Token 类型;省略时匹配所有类型。token_modifiers:要匹配的语义 Token 修饰符列表;须全部命中才算匹配。style:取自当前语法主题的样式列表;取第一个能找到的样式,其后的设置项会覆盖该样式。foreground_color:该类型使用的前景色,十六进制格式(如"#ff0000")。background_color:使用的背景色,十六进制格式(如"#ff0000")。underline:布尔值或十六进制颜色;为true时用文本颜色加下划线。strikethrough:布尔值或十六进制颜色;为true时用文本颜色加删除线。font_weight:取"normal"或"bold"。font_style:取"normal"或"italic"。
多语言支持:language_ids映射
如果语言服务器支持多种语言,可用language_ids将 Zed 语言映射到 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"仓库内 HTML 扩展的 extension.toml 即真实采用了这种写法:声明vscode-html-language-server,并在language_ids中把HTML映射为html、CSS映射为css。
小结
从一份config.toml到一整套.scm查询,再到extension.toml中 Grammar 与 LSP 的注册,Zed 把“语言支持”拆成了清晰、可复用的模块。编写扩展时建议:先用 HTML 扩展 与 GLSL 扩展 作为最小可行模板,逐步补齐高亮、缩进、大纲与注入;接入 LSP 后按需定制语义 Token 样式。若要参考更多内置语言的做法,本仓库的 crates/languages/src 收录了 Zed 各内置语言的完整配置与查询,可作为适配各种语言惯用法的直接范本。
这些注解会被 Assistant 在生成代码修改步骤时使用。
↩
【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考