语言扩展

Zed 中的语言支持包含几个组件

  • 语言元数据和配置
  • 语法
  • 查询
  • 语言服务器

语言元数据

Zed 支持的每种语言都必须在扩展的 languages 目录中的子目录中定义。

该子目录必须包含一个名为 config.toml 的文件,其结构如下:

name = "My Language"
grammar = "my-language"
path_suffixes = ["myl"]
line_comments = ["# "]
  • name (必需) 是将在“选择语言”下拉菜单中显示的易读名称。
  • grammar (必需) 是语法名称。语法是单独注册的,如下所述。
  • path_suffixes 是应与此语言关联的文件后缀数组。与设置中的 file_types 不同,此项不支持 glob 模式。
  • line_comments 是用于识别语言中行注释的字符串数组。这用于 editor::ToggleComments 键绑定:cmd-/|ctrl-/ 用于切换代码行。
  • tab_size 定义此语言使用的缩进/制表符大小 (默认值为 4)。
  • hard_tabs 是用制表符 (true) 还是空格 (false,默认值) 缩进。
  • first_line_pattern 是一个正则表达式,除了 path_suffixes (如上) 或设置中的 file_types 之外,还可以用于匹配应使用此语言的文件。例如,Zed 通过匹配脚本第一行中的 shebang 行来识别 Shell 脚本。
  • debuggers 是用于识别语言中调试器的字符串数组。启动调试器的 New Process Modal 时,Zed 将按照此数组中条目的顺序排列可用的调试器。

语法

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。如果您正在本地开发扩展并希望从本地文件系统加载语法,则可以使用 file:// URL 作为 repository。一个扩展可以通过引用多个 Tree-sitter 存储库来提供多个语法。

Tree-sitter 查询

Zed 使用 Tree-sitter 查询语言生成的语法树来实现以下功能:

  • 语法高亮
  • 括号匹配
  • 代码大纲/结构
  • 自动缩进
  • 代码注入
  • 语法覆盖
  • 文本修订
  • 可运行代码检测
  • 选择类、函数等

以下部分将详细阐述 Tree-sitter 查询如何在 Zed 中实现这些功能,并以 JSON 语法作为指导示例。

语法高亮

在 Tree-sitter 中,highlights.scm 文件定义了特定语法的语法高亮规则。

以下是 JSON 的 highlights.scm 文件中的一个示例:

(string) @string

(pair
  key: (string) @property.json_key)

(number) @number

此查询将字符串、对象键和数字标记为高亮显示。以下是主题支持的捕获的完整列表:

捕获描述
@attribute捕获属性
@boolean捕获布尔值
@comment捕获注释
@comment.doc捕获文档注释
@constant捕获常量
@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捕获类型
@variable捕获变量
@variable.special捕获特殊变量
@variant捕获变体

括号匹配

brackets.scm 文件定义匹配括号。

以下是 JSON 的 brackets.scm 文件中的一个示例:

("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)

此查询识别开括号、大括号和引号。

捕获描述
@open捕获开括号、大括号和引号
@close捕获闭括号、大括号和引号

代码大纲/结构

outline.scm 文件定义代码大纲的结构。

以下是 JSON 的 outline.scm 文件中的一个示例:

(pair
  key: (string (string_content) @name)) @item

此查询捕获对象键以用于大纲结构。

捕获描述
@name捕获对象键的内容
@item捕获整个键值对
@context捕获为大纲项提供上下文的元素
@context.extra捕获大纲项的额外上下文信息
@annotation捕获注释大纲项的节点 (文档注释、属性、装饰器)1
1

这些注释由助手在生成代码修改步骤时使用。

自动缩进

indents.scm 文件定义缩进规则。

以下是 JSON 的 indents.scm 文件中的一个示例:

(array "]" @end) @indent
(object "}" @end) @indent

此查询标记数组和对象的末尾以进行缩进。

捕获描述
@end捕获闭括号和大括号
@indent捕获整个数组和对象以进行缩进

代码注入

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 中,当光标位于 _字符串_ 内时,“-”应被视为完成查询字符。为此,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 中定义的范围是_排他_的。因此在上述情况下,如果光标位于分隔字符串的引号_外部_,则 string 作用域将不会生效。有时,您可能希望使范围_包含_。您可以通过在查询中的捕获名称后添加 .inclusive 后缀来实现此目的。

例如,在 JavaScript 中,我们还禁用了注释中单引号的自动关闭。并且注释作用域必须一直延伸到行注释后的换行符。为了实现这一点,JavaScript 的 overrides.scm 包含以下模式:

(comment) @comment.inclusive

文本对象

textobjects.scm 文件定义了通过文本对象导航的规则。此功能在 Zed v0.165 中添加,目前仅在 Vim 模式下使用。

Vim 提供了两个粒度级别的文件导航。使用 [] 等逐节导航,以及使用 ]m 等逐方法导航。即使不支持函数和类的语言,通过定义类似的概念也能很好地工作。例如,CSS 将规则集定义为方法,将媒体查询定义为类。

对于具有闭包的语言,这些通常不应在 Zed 中算作函数。但这只是尽力而为,因为像 JavaScript 这样的语言在语法上没有区分闭包和顶层函数声明。

对于具有像 C 这样的声明的语言,提供匹配 @class.around@function.around 的查询。如果没有内部,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 文本对象 (很少支持)

例如

; include only the content of the method in the function
(method_definition
    body: (_
        "{"
        (_)* @function.inside
        "}")) @function.around

; match function.around for declarations with no body
(function_signature_item) @function.around

; join all adjacent comments into one
(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 捕获指定运行按钮应在编辑器中显示的位置。其他捕获 (除带下划线前缀的捕获外) 在运行代码时以 ZED_CUSTOM_$(capture_name) 为前缀作为环境变量公开。

捕获描述
@_name捕获“scripts”键
@run捕获脚本名称
@script也捕获脚本名称 (用于不同目的)

语言服务器

Zed 使用 语言服务器协议 提供高级语言支持。

一个扩展可以提供任意数量的语言服务器。要从您的扩展提供语言服务器,请在 extension.toml 中添加一个条目,其中包含您的语言服务器的名称及其适用的语言。languages 列表中的条目必须与该语言的 config.toml 文件中的 name 字段匹配。

[language_servers.my-language-server]
name = "My Language LSP"
languages = ["My Language"]

然后,在您的扩展的 Rust 代码中,实现扩展的 language_server_command 方法。

#![allow(unused)]
fn main() {
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 中的几个可选方法来自定义语言服务器的处理。例如,您可以使用 label_for_completion 方法控制完成的样式。有关方法的完整列表,请参阅 Zed 扩展 API 的 API 文档

多语言支持

如果您的语言服务器支持其他语言,您可以使用 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"