第 12 章:Task、Terminal 与 Debugger——进程能力如何被产品化
Task 把“上下文相关的命令模板”解析成 SpawnInTerminal,Terminal 把 PTY/子进程投影成可交互 Emulator,Debugger 再把 build、launch/attach、DAP session 和编辑器位置串起来。
1. Task 不是 shell command 字符串
Task 分三层:
TaskTemplate
+ TaskContext / TaskVariables
→ ResolvedTask
→ SpawnInTerminalResolvedTask 保存原模板、稳定 TaskId、已替换变量、最终 label 和 spawn 描述。 源码:crates/task/src/task.rs:112-147。
2. TaskContext 从哪里来
Editor/Project 收集:
- 当前文件、相对路径、目录、文件名;
- cursor row/column、selection;
- Language;
- Tree-sitter runnable symbol;
- worktree root;
- Git repo/ref/SHA;
- extension 自定义变量;
- project environment。
变量枚举见 crates/task/src/task.rs:149-201。
因此同一个 cargo test $ZED_SYMBOL 模板在不同 cursor 下生成不同 ResolvedTask/TaskId。
3. Task source 是可组合的
TaskStore 聚合:
- 用户
tasks.json; - project
.zed/tasks.json; - language runnable query;
- extension task provider;
- VS Code task/debug config;
- Debugger build task。
Source 提供模板和优先级;解析阶段统一替换变量和应用 settings。
4. SpawnInTerminal 是执行边界
它包含 command、args、cwd、env、shell、是否复用 tab、是否允许并发、show/hide/reveal、保存策略。 源码:crates/task/src/task.rs:40-80。
Task 逻辑到此停止,不直接依赖 Alacritty 或 Pane。Workspace 的 TerminalProvider 决定在本地、远端、 center item 还是 bottom panel 创建终端。
5. Terminal 的三层结构
PTY / child process
↕ bytes
alacritty_terminal state machine
↕ cells / modes / cursor / selection
terminal::Terminal Entity
↕ Event / RenderableCells
terminal_view::TerminalView / Panel / Element底层 terminal crate 不负责产品布局;terminal_view 才接 Workspace、keymap、tabs 和 GPUI 绘制。
6. PTY 为什么重要
交互程序需要:
- controlling terminal;
- window size change;
- foreground process group;
- ANSI mode;
- Ctrl-C/信号;
- shell prompt 和 job control。
直接 Command::output 无法提供这些语义。Zed 用 PTY 运行普通终端,并把字节送入 Alacritty emulator。
7. Headless fallback
Eval CLI 等无 controlling TTY 环境无法分配 PTY。HeadlessTerminal Global 开启后,Terminal 用 管道启动普通 subprocess,并把 stdout/stderr 泵进 emulator。
源码:crates/terminal/src/terminal.rs:105-117、:3057-3075。
管道不会自动把 LF 变 CRLF,Zed 还显式插入 \r,否则 emulator cursor 下移后不回到列 0。
8. Terminal 关闭是进程树问题
只关闭 PTY master 通常发 SIGHUP,但 foreground job 可能忽略。Zed 先对 shell/foreground process group 发温和信号,等待 grace period,再 SIGKILL 存活进程;时间必须短于 GPUI 全局 shutdown timeout。
源码说明:crates/terminal/src/terminal.rs:78-91。
这说明 UI tab 生命周期必须显式映射到 OS process 生命周期。
9. DAP 分层
| 层 | 对象 |
|---|---|
| 配置 | DebugScenario、launch/attach、build task |
| Adapter | binary、command、locator、capability |
| 协议 | dap::Client、transport、request/event |
| Project | DapStore、breakpoint store、session |
| UI | DebugPanel、Session view、stack/variables/console |
dap_adapters 提供内置 CodeLLDB/GDB/Go/JS/Python 适配;扩展也可通过 Extension API 提供 adapter 和 locator。
10. 启动调试的编排
用户选择 DebugScenario
→ 解析变量 / 选择 process(attach)
→ 可选 build Task
→ locator 根据 build output 找 artifact
→ adapter 获取/下载 debug adapter binary
→ spawn DAP client
→ initialize
→ launch 或 attach
→ setBreakpoints / configurationDone
→ Session 事件驱动 UI 与 Editor调试不是“启动一个进程”而是 Task 和协议状态机的组合。
11. Breakpoint 为什么放 Project
Breakpoint 关联 ProjectPath/Anchor,需要:
- 文件 rename 后仍可追踪;
- Session 启动时发给 adapter;
- 多个 Debug Session 共享用户定义;
- remote Project 发送到 host;
- Editor gutter 观察更新。
因此 BreakpointStore 属于 Project,而不是某个 Editor 或 DebugPanel。
12. StackFrame 如何定位回 Editor
DAP Source/line/column 使用 adapter 路径与 1-based position。Project/DAP layer:
- 根据 remote/local path mapping 找 ProjectPath;
- 打开 Buffer;
- 把 line/column 转 Point/Anchor;
- Workspace 打开或激活 Editor;
- DisplayMap 投影 stack frame line highlight;
- inline values 在当前 frame/context 请求后成为 inlay/block。
13. Local/Remote 再次同构
Remote Project 上 Task/Terminal/DAP 必须在远端执行,但本地仍显示 TerminalView 和 DebugPanel。 执行描述、事件和状态经过 proto;Workspace 的 Item/Panel 不需要另一套远程 UI。
14. 失败路径
- task variable 缺失:模板不可 resolve,向 picker 解释原因;
- command 启动失败:Terminal 保留错误输出与 TaskStatus;
- build task 失败:不进入 launch;
- adapter 不支持请求:capability gate;
- remote disconnect:Session 转 stopped/disconnected;
- source path 无法映射:仍可在 loaded source/反汇编视图显示;
- tab 关闭但进程存活:执行信号升级和清理。
15. 架构经验
- 模板、解析结果、执行描述分层;
- 进程后端和终端 UI 分 crate;
- Debugger 复用 Task 作为 build 阶段,不再造命令系统;
- Breakpoint/Session 属于 Project,Panel 只是投影;
- headless/remote 通过替换执行后端复用 emulator 和上层状态。