超过2000名开发者提出需求,我们做到了。

Zed中的调试功能现已成为现实——这是迈向Zed 1.0的一大步。

Zed原生调试器简介。

概述

我们着手构建一个调试器,主要关注三个方面

  • 快速:减少上下文切换时间,将更多时间用于调试
  • 熟悉:与 Zed 的设计语言保持一致,并支持典型调试流程中的所有预期功能
  • 可配置:您可以自定义用户界面、键盘绑定、调试配置等

开箱即用,Zed 支持调试流行的语言,包括 Rust、C/C++、JavaScript、Go 和 Python。通过我们的扩展系统,Zed 可以支持任何实现 调试适配器协议 (DAP) 的调试适配器。

为了简化设置过程,我们引入了定位器,这是一个将构建配置转换为调试配置的系统。这意味着您可以在 tasks.json 中编写一次构建任务,然后从 debug.json 中引用它——或者更好的是,依赖 Zed 的自动配置。

Zed 会自动在内置或语言服务器生成的运行程序上运行定位器,因此在许多情况下,您甚至不需要编写调试配置即可启动和运行。

我们目前支持 Cargo、Python、JavaScript 和 Go 的定位器,未来还会支持更多。有关配置调试会话的更多信息,请参阅我们的文档

进入调试会话后,Zed 可以轻松检查程序的各种状态,例如线程、变量、断点、调用堆栈等。

设置一些断点并在调试会话中运行测试。

调试器面板也是完全可定制的,只需按照您想要的任何顺序拖放和重新排列选项卡;您甚至可以移动调试面板以适应您的工作流程。

Zed 还支持键盘驱动的调试,适用于喜欢将手放在键盘上的用户。您可以单步执行代码、切换断点以及导航调试会话,而无需触摸鼠标。

仅使用键盘导航调试器界面。

特别鸣谢

该调试器最初是一个社区主导的项目,取得了一些令人印象深刻的统计数据:8个月的开发、977次提交和25k+行代码。社区构建了核心基础,使得今天的发布成为可能。

特别感谢 Remco Smits 在这个项目中承担了大量的繁重工作——您的贡献对我们走到这一步至关重要。

幕后

Zed 的调试器通过调试适配器协议支持多种语言的调试。但仅仅实现协议是不够的——我们需要一个能够扩展到协作调试、支持扩展并有效缓存和管理调试适配器响应的架构。

为了实现这一点,我们构建了一个两层架构:一个直接与调试适配器通信的数据层,以及一个从数据层获取数据以渲染界面的 UI 层。

/// All functions are cheap to call, as they grab current state of the debug session and schedule refreshing on a background
/// thread if that state is outdated.
pub fn modules(&mut self, cx: &mut Context<Self>) -> &[Module] {
    /// Kick off a fresh request to a DAP for modules list if we don't have an up-to-date state.
    /// This is a no-op in case we've ran that request already. In case we did not, it kicks off a background task.
    self.fetch(
        /// We cache request results based on it's arguments. `Modules` request does not take any arguments
        dap_command::ModulesCommand,
        /// Callback invoked with the result of a request.
        |this, result, cx| {
            let Some(result) = result.log_err() else {
                return;
            };
 
            this.modules = result;
            cx.emit(SessionEvent::Modules);
            cx.notify();
        },
        cx,
    );
 
    /// Returns a current list of modules; it might be outdated at the time the new request is underway,
    /// but once it is done, the return value of this function will reflect that.
    &self.modules
}
/// This function is called from the Module list render function in the UI layer whenever the data layer invalidates the module list state.
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
    /// Setting the task drops any current work in progress that is out of date
    self._rebuild_task = Some(cx.spawn(async move |this, cx| {
        this.update(cx, |this, cx| {
            /// The UI layer queries the data layer for modules and clones the data
            let modules = this
                .session
                .update(cx, |session, cx| session.modules(cx).to_owned());
            this.entries = modules;
            cx.notify();
        })
        .ok();
    }));
}

这种分离意味着 UI 层只请求它需要的数据,允许数据层延迟获取信息并避免不必要的请求。它还使数据层完全负责维护会话状态、缓存响应和使过期数据失效。这种架构将使实现协作调试变得显著更容易,因为相同的 UI 代码可以在多人会话中重用——并且我们只通过网络发送必要的数据,从而节省带宽。

开箱即用地支持所有调试适配器是不现实的——有超过70种 DAP 实现,每种都有其独特的怪癖。为了解决这个问题,我们扩展了 Zed 的扩展 API 以支持调试器集成。

    /// Returns the debug adapter binary for the specified adapter name and configuration.
    fn get_dap_binary(
        &mut self,
        _adapter_name: String,
        _config: DebugTaskDefinition,
        _user_provided_debug_adapter_path: Option<String>,
        _worktree: &Worktree,
    ) -> Result<DebugAdapterBinary, String> {
        Err("`get_dap_binary` not implemented".to_string())
    }
 
    /// Determines whether the specified adapter configuration should *launch* a new debuggee process
    /// or *attach* to an existing one. This function should not perform any further validation (outside of determining the kind of a request).
    /// This function should return an error when the kind cannot be determined (rather than fall back to a known default).
    fn dap_request_kind(
        &mut self,
        _adapter_name: String,
        _config: serde_json::Value,
    ) -> Result<StartDebuggingRequestArgumentsRequest, String> {
        Err("`dap_request_kind` not implemented".to_string())
    }
    /// Converts a high-level definition of a debug scenario (originating in a new session UI) to a "low-level" configuration suitable for a particular adapter.
    ///
    /// In layman's terms: given a program, list of arguments, current working directory and environment variables,
    /// create a configuration that can be used to start a debug session.
    fn dap_config_to_scenario(&mut self, _config: DebugConfig) -> Result<DebugScenario, String> {
        Err("`dap_config_to_scenario` not implemented".to_string())
    }
 
    /// Locators are entities that convert a Zed task into a debug scenario.
    ///
    /// They can be provided even by extensions that don't provide a debug adapter.
    /// For all tasks applicable to a given buffer, Zed will query all locators to find one that can turn the task into a debug scenario.
    /// A converted debug scenario can include a build task (it shouldn't contain any configuration in such case); a build task result will later
    /// be resolved with [`Extension::run_dap_locator`].
    ///
    /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator:
    /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with
    /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope.
    /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function
    /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user
    /// found the artifact path by themselves.
    ///
    /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of
    /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case.
    /// This might be of particular relevance to interpreted languages.
    fn dap_locator_create_scenario(
        &mut self,
        _locator_name: String,
        _build_task: TaskTemplate,
        _resolved_label: String,
        _debug_adapter_name: String,
    ) -> Option<DebugScenario> {
        None
    }
 
    /// Runs the second phase of locator resolution.
    /// See [`Extension::dap_locator_create_scenario`] for a hefty comment on locators.
    fn run_dap_locator(
        &mut self,
        _locator_name: String,
        _build_task: TaskTemplate,
    ) -> Result<DebugRequest, String> {
        Err("`run_dap_locator` not implemented".to_string())
    }

通过扩展添加 DAP 支持涉及定义与我们的 JSON 服务器集成的自定义模式、实现下载和启动适配器的逻辑、处理调试配置以添加合理的默认值,以及与定位器集成以进行自动配置。这种设计遵循我们对 LSP 扩展的方法,让扩展作者可以完全控制将自己的调试适配器以最小的摩擦引入 Zed。

我们还希望行内变量值能开箱即用。令人惊讶的是,行内值请求语言服务器协议 (LSP) 的一部分,而不是 DAP。使用行内值方法会限制 Zed 只能为与 LSP 集成的 DAP 显示行内值,而这样的 DAP 并不多。一个朴素的解决方法是使用正则表达式匹配源代码和调试器值之间的变量名,但这在处理作用域和注释时会迅速失效。相反,我们转向了 Tree-sitter。毕竟 Zed 是由 Tree-sitter 的创建者构建的!

An inline value example.
行内值示例。

通过 Tree-sitter 查询,我们可以准确识别当前执行范围内的变量,并通过 .scm 文件轻松支持任何语言,而无需依赖语言服务器协议 (LSP) 服务器与调试适配器紧密集成。发布时,Python、Rust 和 Go 支持行内值。更多语言将在未来几周内得到支持。

下一步

当我们着手构建调试器时,我们希望它使用起来无缝、不碍事,并符合 Zed 的高质量标准。现在我们已经构建了一个与任何调试适配器兼容的坚实基础,我们已准备好探索和实现高级功能,例如

  • 新视图:虽然我们支持所有基本视图,但我们计划添加更多高级视图,例如监视列表、内存视图、反汇编视图和堆栈跟踪视图
  • 自动配置:我们将增加对更多语言和构建系统的支持
  • 完善和更多:请通过DiscordZed 的 GitHub 存储库联系我们,让我们知道您的想法!

正在寻找更好的编辑器吗?

您今天就可以在 macOS、Windows 或 Linux 上试用 Zed。立即下载


我们正在招聘!

如果您对我们博客中涵盖的主题充满热情,请考虑加入我们的团队,帮助我们实现软件开发的未来。