ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Zed 语言扩展开发指南:从 config.toml 到 Tree-sitter 查询与 LSP 集成

Zed 语言扩展开发指南:从 config.toml 到 Tree-sitter 查询与 LSP 集成 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/zedZed 是一套开放的语言基础设施编辑器自身内置的语言支持与第三方扩展语言包共享同一套“语言元数据 Tree-sitter 语法 LSP 服务器”三层架构。本文以 Zed 仓库中官方文档 languages.md 为骨架结合仓库内真实扩展HTML、GLSL、Test Extension讲解如何为一种新语言编写config.toml、注册 Grammar、编写各类 Tree-sitter.scm查询以及如何接入语言服务器与语义化 TokenSemantic Tokens。读完你可以独立为 Zed 打造一个具备语法高亮、缩进、大纲、括号匹配与 LSP 补全能力的完整语言扩展。语言支持的四个组成部分Zed 中一种语言的支持由四层组成扩展开发需要逐一提供或配置语言元数据与配置Language metadata and configuration——即config.toml声明语言名称、语法、文件后缀、注释风格、缩进等信息语法Grammar——基于 Tree-sitter 解析库的语法定义在extension.toml中单独注册查询Queries——一系列.scmTree-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_innot_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 Grammarextension.toml 中的[grammars.*]Zed 使用 Tree-sitter 解析库提供内置的语言级特性。许多语言都有现成语法也可以自行开发语法Tree-sitter 官方写作指南提供了从零编写语法的路径。前面提到扩展中定义的每种语言必须指定用于解析的 Grammar 名称而这些 Grammar 要在扩展根目录的extension.toml中单独注册例如[grammars.gleam] repository https://github.com/gleam-lang/tree-sitter-gleam rev 58b7cac8fc14c92b0677c542610d8738c373fa81repository指定加载 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.scmTree-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捕获链接 URInumber捕获数值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 symboltag捕获标签如 HTML 标签tag.doctype捕获文档类型声明如 HTML doctypetext.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.scmbrackets.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.scmoutline.scm定义代码大纲的结构。JSON 的示例捕获对象键生成大纲条目(pair key: (string (string_content) name)) itemCapture描述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_patternsvalid_afterdecrease_indent_patterns适用于“应与其所属代码块对齐而非无条件左移一级”的行。做法分两步先在indents.scm中用带命名的start.name捕获标记该代码块的起点(if_statement) start.if然后在config.toml中把该捕获的后缀名列入valid_afterdecrease_indent_patterns [ { pattern ^\\s*else\\b, valid_after [if] }, ]此时以else开头的行会与最近一个“处于相同或更低缩进层级”的start.if对齐若找不到匹配的代码块缩进保持不变。注意start.if这类命名捕获只用于标记代码块与start不同它们不会改变indent的范围。Zed 会按顺序检查规则命中第一条pattern后即停止因此较具体的模式应写在较通用的模式之前。代码注入injections.scminjections.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.tomlword_characters [#, $] [overrides.string] completion_query_characters [-]也可以在指定作用域内禁用某些自动闭合括号。例如阻止字符串内部自动闭合可把如下内容写入 JavaScript 的config.tomlbrackets [ { 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.scmtextobjects.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.scmredactions.scm定义文本脱敏规则。协作与共享屏幕时Zed 会以脱敏模式渲染某些语法节点避免泄露敏感数据。JSON 的示例(pair value: (number) redact) (pair value: (string) redact) (array (number) redact) (array (string) redact)Capture描述redact捕获需脱敏的值该查询将键值对与数组中的数值、字符串标记为脱敏对象。可运行代码检测runnables.scmrunnables.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接入 LSPZed 通过语言服务器协议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, ) - Resultzed::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 TokensZed 支持使用语言服务器上报的语义 Token 进行语法高亮。该特性默认关闭可在设置文件中启用{ // 全局启用语义 Token并叠加 tree-sitter 高亮 semantic_tokens: combined, // 或者按语言单独指定 languages: { Rust: { // 不使用 tree-sitter只用 LSP 语义 Token semantic_tokens: full } } }semantic_tokens设置取值off默认不向语言服务器请求语义 Tokencombined将 LSP 语义 Token 与 tree-sitter 高亮叠加使用full仅使用 LSP 语义 Token取代 tree-sitter 高亮。扩展自带的语义 Token 规则语言扩展可为自家语言服务器上报的自定义 Token 类型提供默认的语义 Token 规则。做法是在语言目录与config.toml同级放置semantic_token_rules.jsonmy-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_typeLSP 规范定义的语义 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),仅供参考
返回列表