您是否曾想在 Zed 内部执行代码?运行测试、或 linter、或编译器、或脚本、或一行 shell 命令?
观看
您刚刚看到的是我使用 Zed 任务在 Zed 内部执行 Go 测试,并将当前函数的名称传递给 go test 命令。
任务,作为一项新功能,早在二月就已在 Zed 中登陆,版本为 v0.124.7。
但从那时起,它们就得到了 Piotr、Kirill 和 Mikayla 的持续改进。现在,在最新的 Zed 预览版 v0.136 中,它们确实令人印象深刻。
它们以最好的方式简单,同时又功能强大。它们还在底层使用了一些非常整洁的 Tree-sitter 技术,这就是我想要深入研究它们的原因。
伴随视频
语法感知任务生成与 Tree-Sitter
这篇博文附带一个1小时的配套视频,其中 Thorsten 与 Piotr 和 Kirill(以及 Mikayla)讨论了他们如何构建任务。他们一起探讨了运行任务的所有不同方式,然后深入研究了它们的实现。
运行任务
首先。如何运行任务?打开 Zed 后,按下 cmd-shift-p 打开命令面板,然后输入 task: spawn。
您会得到另一个模式窗口,在其中输入要执行的命令。opt-return 启动任务。像这样
现在,再次按下 cmd-shift-p,但这次输入 task: rerun。
顾名思义,这会重新运行您执行的最后一个任务。如果您运行了多个不同的任务,则始终会重新运行最后一个任务。(除了使用 opt-return 启动任务,您还可以使用 cmd-opt-return,这将导致任务作为临时任务运行——一个不会被标记为“上次运行任务”的任务。)
如果您认为输入太多,“哦我的手腕好痛”:有快捷键可以启动和重新运行任务——opt-shift-t 绑定到 task: spawn,opt-t 绑定到 task: rerun。
好的,到目前为止一切顺利。每个曾经想用快捷键执行代码的人都向后靠着,叹了口气:“终于来了。”
还有更多。
任务变量
在上面的简短介绍视频中,您看到我使用 $ZED_SYMBOL 来指代运行任务时光标所在的函数。
$ZED_SYMBOL 由 Tree-sitter 提供支持,并填充为包含当前光标位置的最后一个符号的名称。这应该对应于您在 Zed 窗格顶部面包屑中看到的最后一个符号。
还有更多此类变量可用
$ZED_FILE指代当前打开文件的绝对路径$ZED_ROW和$ZED_COLUMN包含光标的行/列$ZED_WORKTREE_ROOT是 Zed 中工作树根文件夹的绝对路径
您可以在此处找到完整且最新的变量列表,但我想在此处强调其中一个:$ZED_SELECTED_TEXT。
使用任务评估代码
$ZED_SELECTED_TEXT 包含——没错,您猜到了——当前选定的文本。这听起来可能没什么,但它强大。
看一看
在这个视频中,我正在启动一个任务,它将 $ZED_SELECTED_TEXT 传递给 PostgreSQL CLI 工具 psql。
我选择第一条 SQL 语句,启动任务,然后选择下一条,重新运行任务,选择再下一条,重新运行任务,选择最后一条,重新运行任务。
如果您注意到了或已经试玩了任务,您可能会想:等等,您是如何重新运行任务的,以使 $ZED_SELECTED_TEXT 始终包含最新的选择,而不是您首次运行任务时的选择?
答案在于我添加到我个人 Zed keymap.json 中的这个快捷键绑定
[
{
"context": "EmptyPane || SharedScreen || vim_operator == none && !VimWaiting && vim_mode != insert",
"bindings": {
", r e": ["task::Rerun", { "reevaluate_context": true }]
}
}
]忽略 "context" 和 Vim 模式特定内容,重要的是这一行:{ "reevaluate_context": true }。
将 reevaluate_context 设置为 true 后,我总是可以重新运行上一个任务,并让变量 — $ZED_SELECTED_TEXT,或 $ZED_FILE,... — 重新评估。
并且 还有更多 task::Rerun 变量:allow_concurrent_runs 和 use_new_terminal。在配套视频中,您可以观看 Piotr 和 Kirill 如何向我解释这些变量以及如何最好地使用它们。
我告诉您,这太强大了:通过任务,您可以评估完整文件、脚本行、选择等等……无限可能!或者说,取决于您在 shell 中可以运行什么。
但也许您又在想“哦,我的手太累了”,因为您看到我输入这些命令和变量,并认为肯定有更好的方法?确实有。
定义任务
您可以通过使用任务模板来定义任务。这些是 JSON 文件,您可以在其中定义多个不同的任务并使用任务变量。
任务模板可以放在两个不同的地方
- 在项目根文件夹中的
.zed/tasks.json文件中(使用zed: open local tasks创建/打开该文件) - 在全局
~/.config/zed/tasks.json文件中(使用zed: open tasks创建/打开该文件)
这是一个此类文件的示例
[
{
"label": "My cool loop",
"command": "for i in {1..5}; do echo \"Hello $ZED_FILE $ZED_ROW - $i/5\"; sleep 1; done"
},
{
"label": "ruby eval: '$ZED_SELECTED_TEXT'",
"command": "ruby -e '$ZED_SELECTED_TEXT'",
"use_new_terminal": false
},
{
"label": "go test - current function",
"command": "go test . -run $ZED_SYMBOL",
"reveal": "always"
},
{
"label": "Number of dotfiles",
"command": "find . -name '.*' -depth 1 | wc -l",
"cwd": "/Users/thorstenball"
}
]在 .zed/tasks.json 中有了这个文件,当我运行 task: spawn 时,我得到以下模式窗口

如果您像这样在 tasks.json 文件中使用 label 定义任务,那么您还可以创建快捷键来启动特定任务。例如
{
"context": "EmptyPane || SharedScreen || vim_operator == none && !VimWaiting && vim_mode != insert",
"bindings": {
", r t": ["task::Spawn", { "task_name": "My cool loop" }]
}
}这将启动上面提到的 My cool loop 任务。
所以:您的可怜手指是安全的!减少打字。不仅因为您只需编写大多数任务定义一次,还因为有时您根本不必编写它们。
特定于语言的任务
在 Zed 中,越来越多的语言已经定义了任务。例如,当您在 Rust 文件中运行 task: spawn 时,您会看到以下内容

这些任务用于运行测试、检查和 lint 代码、运行代码等等。
语言扩展可以定义自己的 tasks.json,然后将其呈现给用户。
- Elixir 已经有了
tasks.json - Gleam 也已拥有
tasks.json - @RemcoSmitsDev 正在 为 PHP 添加
tasks.json
这些定义也没有什么特别之处。它们和您可以运行的任务一样,只不过它们附带了语言扩展。如果您现在想知道是否应该打开一个 PR 来为您最喜欢的语言添加一个 tasks.json 文件:是的,请务必这样做!
(值得注意的是,Rust 作为我们在内部使用最多的语言,也是我们的试验台,它有一些特别之处:Rust 动态定义了一个 $RUST_PACKAGE 变量。扩展目前还不能做到这一点,但计划是让扩展也能够定义自己的变量。)
而且,再说一遍:还有更多。
可运行项
如果您使用最新版本的 Zed 打开 Rust 文件,您不仅会得到一个漂亮的任务列表来运行,还会得到这个

看到左侧边栏上的小播放按钮了吗?
是的,您可以点击它们
您在这里看到的也是任务,但是 Zed 如何知道将播放按钮放在测试旁边以运行任务呢?
答案——再次——是 Tree-sitter。它是这样工作的。
Zed 中的每个语言扩展都可以附带一个名为 runnables.scm 的文件,其中包含 Tree-sitter 查询,用于捕获可运行的语法树节点:测试函数、main 函数——实际上,任何可运行的东西。
这是当前的 Rust runnables.scm
(
(attribute_item (attribute
[((identifier) @_attribute)
(scoped_identifier (identifier) @_attribute)
])
(#eq? @_attribute "test"))
.
(attribute_item) *
.
(function_item
name: (_) @run)
) @rust-test如果你以前从未见过 Scheme 或 Tree-sitter 查询,这看起来会很陌生。不过,它的作用并不复杂。
Tree-sitter 查询描述了一种语法节点模式(在模式匹配的意义上),用于与语法树进行匹配。此特定查询匹配一个语法树节点,该节点首先具有一个标识符为 "test" 的 attribute_item。然后,该查询允许任意数量的其他属性项——(attribute item) *——然后它要求存在一个 function_item,它将该项的 name 属性放入变量 @run 中。如果语法节点与此模式匹配,则将其标记为 @rust-test。
当您在 Zed 中运行 debug: open syntax tree view 时,您可以看到给定文件的语法树。对于此测试文件,我们可以看到测试函数匹配 runnables.scm 文件中描述的模式

一旦节点被标记为 @rust-test,作为可运行项,剩下的问题是:我们如何运行它?
这就是 tasks.json 再次发挥作用的地方。这是一个示例 tasks.json
{
"label": "cargo test function",
"command": "cargo",
"args": ["test", "$ZED_SYMBOL"],
"tags": ["rust-test"]
}这里的新内容是 "tags" 属性。它包含 rust-test,这也正是我们标记可运行语法节点的方式,以及 runnables.scm 和任务连接的方式
runnables.scm中的查询可以匹配任何“可运行”的语法节点并为其添加标签tasks.json文件中的任务定义可以包含tags- 如果 Zed 找到节点与任务定义之间的匹配项,它会在每个节点旁边放置一个播放按钮,以便用户可以使用定义的任务运行它。
我再说一遍:这很强大,因为任何东西都可以被标记为可运行,而且任何东西都可以作为任务执行。想象一下可能性!
您不仅可以标记测试函数,还可以标记整个测试套件。或者,您可以用不同的方式标记不同的测试,这样您就可以拥有 integration-test 和 unit-test,或者 fast-test 和 slow-test。
或者您可以标记 main 函数、SQL 语句,或者 Markdown 文件中的 dot 代码块,用 graphviz 执行,或者……
换句话说:去试试任务,告诉我们您的想法,祝您 Zed 扩展开发愉快!