您是否曾经想在 Zed 内部执行代码?运行测试、或者一个 linter、或者编译器、或者一个脚本,或者一个 shell 单行命令?
观看
您刚才看到的是我使用 Zed *Tasks* 从 Zed 内部执行 Go 测试,并将当前函数的名称传递给 go test
命令。
作为一个新功能,Tasks 最早于二月份在 Zed 中发布,版本为 v0.124.7。
但从那时起,Piotr、Kirill 和 Mikayla 一直在不断改进它们。现在,在最新的 Zed Preview 版本 v0.136 中,它们坦率地说令人印象深刻。
它们简单到了极致,同时又功能强大。它们还使用了一些非常棒的 Tree-sitter 技术,这就是我想深入研究它们的原因。
配套视频
使用 Tree-Sitter 的语法感知任务生成
这篇文章附带了一个 1 小时的配套视频,Thorsten 在视频中与 Piotr 和 Kirill 进行了交谈,他们(与 Mikayla 一起)构建了 Tasks。他们一起探索了运行 Tasks 的所有不同方式,然后深入研究了它们的实现。
运行任务
首先,如何运行任务?打开 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正在添加
tasks.json
到PHP
这些定义也没有什么特别之处。它们是可以运行的相同的任务,只是它们带有语言扩展。如果你现在想知道是否应该打开一个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查询描述了一种语法节点模式(在模式匹配的意义上),用于匹配语法树。这个特定的查询匹配一个语法树节点,该节点首先具有一个attribute_item
,其标识符为"test"
。然后,该查询允许任意数量的其他属性项——(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
和 Tasks 连接的方式。
runnables.scm
中的查询可以匹配任何“可运行的”语法节点,并给它们一个标签。tasks.json
文件中的任务定义可以包含tags
。- 如果 Zed 在节点和任务定义之间找到匹配项,它会在每个节点旁边放置一个播放按钮,以便用户可以使用定义的任务来运行它。
我再说一遍:这非常强大,因为任何东西都可以被标记为可运行的,并且任何东西都可以作为任务执行。想象一下可能性!
您可以标记整个测试套件,而不仅仅是测试函数。或者,您可以以不同的方式标记不同的测试,这样您就可以拥有 integration-test
和 unit-test
,或者 fast-test
和 slow-test
。
或者您可以标记 main
函数、SQL 语句或 在 Markdown 文件中使用 graphviz 执行的 dot
代码块,或者...
换句话说:快去试试 Tasks,让我们知道您的想法,并愉快地进行 Zed 扩展的开发!