extension: Update DAP extension API (#32448)

Piotr Osiewicz created

- DAP schemas will be stored in `debug_adapters_schemas` subdirectory in
extension work dir.
- Added Debug Config integration and such.

Release Notes:

- N/A

Change summary

Cargo.lock                                                       |   1 
crates/dap/src/adapters.rs                                       |   4 
crates/dap/src/registry.rs                                       |   5 
crates/dap_adapters/src/codelldb.rs                              |   2 
crates/dap_adapters/src/gdb.rs                                   |   2 
crates/dap_adapters/src/go.rs                                    |   2 
crates/dap_adapters/src/javascript.rs                            |   2 
crates/dap_adapters/src/php.rs                                   |   2 
crates/dap_adapters/src/python.rs                                |   2 
crates/dap_adapters/src/ruby.rs                                  |   2 
crates/debug_adapter_extension/Cargo.toml                        |   1 
crates/debug_adapter_extension/src/debug_adapter_extension.rs    |  18 
crates/debug_adapter_extension/src/extension_dap_adapter.rs      |  27 
crates/debug_adapter_extension/src/extension_locator_adapter.rs  |  50 
crates/extension/src/extension.rs                                |  26 
crates/extension/src/extension_builder.rs                        |  20 
crates/extension/src/extension_host_proxy.rs                     |   9 
crates/extension/src/extension_manifest.rs                       |   4 
crates/extension/src/types/dap.rs                                |   5 
crates/extension_api/src/extension_api.rs                        |  70 
crates/extension_api/wit/since_v0.6.0/dap.wit                    |  65 
crates/extension_api/wit/since_v0.6.0/extension.wit              |   9 
crates/extension_host/benches/extension_compilation_benchmark.rs |   1 
crates/extension_host/src/extension_host.rs                      |   5 
crates/extension_host/src/extension_store_test.rs                |   3 
crates/extension_host/src/wasm_host.rs                           |  70 
crates/extension_host/src/wasm_host/wit.rs                       |  88 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs          | 170 +
28 files changed, 619 insertions(+), 46 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4242,6 +4242,7 @@ dependencies = [
  "gpui",
  "serde_json",
  "task",
+ "util",
  "workspace-hack",
 ]
 

crates/dap/src/adapters.rs 🔗

@@ -368,7 +368,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
         }
     }
 
-    async fn dap_schema(&self) -> serde_json::Value;
+    fn dap_schema(&self) -> serde_json::Value;
 
     fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
         None
@@ -394,7 +394,7 @@ impl DebugAdapter for FakeAdapter {
         DebugAdapterName(Self::ADAPTER_NAME.into())
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         serde_json::Value::Null
     }
 

crates/dap/src/registry.rs 🔗

@@ -14,7 +14,7 @@ use crate::{
 };
 use std::{collections::BTreeMap, sync::Arc};
 
-/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
+/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
 #[async_trait]
 pub trait DapLocator: Send + Sync {
     fn name(&self) -> SharedString;
@@ -67,13 +67,12 @@ impl DapRegistry {
     pub async fn adapters_schema(&self) -> task::AdapterSchemas {
         let mut schemas = AdapterSchemas(vec![]);
 
-        // Clone to avoid holding lock over await points
         let adapters = self.0.read().adapters.clone();
 
         for (name, adapter) in adapters.into_iter() {
             schemas.0.push(AdapterSchema {
                 adapter: name.into(),
-                schema: adapter.dap_schema().await,
+                schema: adapter.dap_schema(),
             });
         }
 

crates/dap_adapters/src/codelldb.rs 🔗

@@ -133,7 +133,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
         })
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "properties": {
                 "request": {

crates/dap_adapters/src/gdb.rs 🔗

@@ -63,7 +63,7 @@ impl DebugAdapter for GdbDebugAdapter {
         })
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "oneOf": [
                 {

crates/dap_adapters/src/go.rs 🔗

@@ -95,7 +95,7 @@ impl DebugAdapter for GoDebugAdapter {
         Some(SharedString::new_static("Go").into())
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         // Create common properties shared between launch and attach
         let common_properties = json!({
             "debugAdapter": {

crates/dap_adapters/src/javascript.rs 🔗

@@ -182,7 +182,7 @@ impl DebugAdapter for JsDebugAdapter {
         })
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "oneOf": [
                 {

crates/dap_adapters/src/php.rs 🔗

@@ -110,7 +110,7 @@ impl PhpDebugAdapter {
 
 #[async_trait(?Send)]
 impl DebugAdapter for PhpDebugAdapter {
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "properties": {
                 "request": {

crates/dap_adapters/src/python.rs 🔗

@@ -257,7 +257,7 @@ impl DebugAdapter for PythonDebugAdapter {
         })
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "properties": {
                 "request": {

crates/dap_adapters/src/ruby.rs 🔗

@@ -49,7 +49,7 @@ impl DebugAdapter for RubyDebugAdapter {
         Ok(StartDebuggingRequestArgumentsRequest::Launch)
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
+    fn dap_schema(&self) -> serde_json::Value {
         json!({
             "type": "object",
             "properties": {

crates/debug_adapter_extension/Cargo.toml 🔗

@@ -12,6 +12,7 @@ dap.workspace = true
 extension.workspace = true
 gpui.workspace = true
 serde_json.workspace = true
+util.workspace = true
 task.workspace = true
 workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
 

crates/debug_adapter_extension/src/debug_adapter_extension.rs 🔗

@@ -1,4 +1,5 @@
 mod extension_dap_adapter;
+mod extension_locator_adapter;
 
 use std::sync::Arc;
 
@@ -6,6 +7,9 @@ use dap::DapRegistry;
 use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy};
 use extension_dap_adapter::ExtensionDapAdapter;
 use gpui::App;
+use util::ResultExt;
+
+use crate::extension_locator_adapter::ExtensionLocatorAdapter;
 
 pub fn init(extension_host_proxy: Arc<ExtensionHostProxy>, cx: &mut App) {
     let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx);
@@ -30,11 +34,21 @@ impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy {
         &self,
         extension: Arc<dyn extension::Extension>,
         debug_adapter_name: Arc<str>,
+    ) {
+        if let Some(adapter) = ExtensionDapAdapter::new(extension, debug_adapter_name).log_err() {
+            self.debug_adapter_registry.add_adapter(Arc::new(adapter));
+        }
+    }
+
+    fn register_debug_locator(
+        &self,
+        extension: Arc<dyn extension::Extension>,
+        locator_name: Arc<str>,
     ) {
         self.debug_adapter_registry
-            .add_adapter(Arc::new(ExtensionDapAdapter::new(
+            .add_locator(Arc::new(ExtensionLocatorAdapter::new(
                 extension,
-                debug_adapter_name,
+                locator_name,
             )));
     }
 }

crates/debug_adapter_extension/src/extension_dap_adapter.rs 🔗

@@ -1,6 +1,10 @@
-use std::{path::PathBuf, sync::Arc};
+use std::{
+    path::{Path, PathBuf},
+    str::FromStr,
+    sync::Arc,
+};
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use async_trait::async_trait;
 use dap::adapters::{
     DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
@@ -12,17 +16,26 @@ use task::{DebugScenario, ZedDebugConfig};
 pub(crate) struct ExtensionDapAdapter {
     extension: Arc<dyn Extension>,
     debug_adapter_name: Arc<str>,
+    schema: serde_json::Value,
 }
 
 impl ExtensionDapAdapter {
     pub(crate) fn new(
         extension: Arc<dyn extension::Extension>,
         debug_adapter_name: Arc<str>,
-    ) -> Self {
-        Self {
+    ) -> Result<Self> {
+        let schema = std::fs::read_to_string(extension.path_from_extension(
+            &Path::new("debug_adapter_schemas").join(debug_adapter_name.as_ref()),
+        ))
+        .with_context(|| format!("Failed to read debug adapter schema for {debug_adapter_name}"))?;
+        let schema = serde_json::Value::from_str(&schema).with_context(|| {
+            format!("Debug adapter schema for {debug_adapter_name} is not a valid JSON")
+        })?;
+        Ok(Self {
             extension,
             debug_adapter_name,
-        }
+            schema,
+        })
     }
 }
 
@@ -61,8 +74,8 @@ impl DebugAdapter for ExtensionDapAdapter {
         self.debug_adapter_name.as_ref().into()
     }
 
-    async fn dap_schema(&self) -> serde_json::Value {
-        self.extension.get_dap_schema().await.unwrap_or_default()
+    fn dap_schema(&self) -> serde_json::Value {
+        self.schema.clone()
     }
 
     async fn get_binary(

crates/debug_adapter_extension/src/extension_locator_adapter.rs 🔗

@@ -0,0 +1,50 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
+use extension::Extension;
+use gpui::SharedString;
+use std::sync::Arc;
+use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
+
+pub(crate) struct ExtensionLocatorAdapter {
+    extension: Arc<dyn Extension>,
+    locator_name: SharedString,
+}
+
+impl ExtensionLocatorAdapter {
+    pub(crate) fn new(extension: Arc<dyn extension::Extension>, locator_name: Arc<str>) -> Self {
+        Self {
+            extension,
+            locator_name: SharedString::from(locator_name),
+        }
+    }
+}
+
+#[async_trait]
+impl DapLocator for ExtensionLocatorAdapter {
+    fn name(&self) -> SharedString {
+        self.locator_name.clone()
+    }
+    /// Determines whether this locator can generate debug target for given task.
+    async fn create_scenario(
+        &self,
+        build_config: &TaskTemplate,
+        resolved_label: &str,
+        adapter: &DebugAdapterName,
+    ) -> Option<DebugScenario> {
+        self.extension
+            .dap_locator_create_scenario(
+                self.locator_name.as_ref().to_owned(),
+                build_config.clone(),
+                resolved_label.to_owned(),
+                adapter.0.as_ref().to_owned(),
+            )
+            .await
+            .ok()
+            .flatten()
+    }
+
+    async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
+        Err(anyhow::anyhow!("Not implemented"))
+    }
+}

crates/extension/src/extension.rs 🔗

@@ -14,6 +14,7 @@ use fs::normalize_path;
 use gpui::{App, Task};
 use language::LanguageName;
 use semantic_version::SemanticVersion;
+use task::{SpawnInTerminal, ZedDebugConfig};
 
 pub use crate::extension_events::*;
 pub use crate::extension_host_proxy::*;
@@ -144,7 +145,30 @@ pub trait Extension: Send + Sync + 'static {
         worktree: Arc<dyn WorktreeDelegate>,
     ) -> Result<DebugAdapterBinary>;
 
-    async fn get_dap_schema(&self) -> Result<serde_json::Value>;
+    async fn dap_request_kind(
+        &self,
+        dap_name: Arc<str>,
+        config: serde_json::Value,
+    ) -> Result<StartDebuggingRequestArgumentsRequest>;
+
+    async fn dap_config_to_scenario(
+        &self,
+        config: ZedDebugConfig,
+        worktree: Arc<dyn WorktreeDelegate>,
+    ) -> Result<DebugScenario>;
+
+    async fn dap_locator_create_scenario(
+        &self,
+        locator_name: String,
+        build_config_template: BuildTaskTemplate,
+        resolved_label: String,
+        debug_adapter_name: String,
+    ) -> Result<Option<DebugScenario>>;
+    async fn run_dap_locator(
+        &self,
+        locator_name: String,
+        config: SpawnInTerminal,
+    ) -> Result<DebugRequest>;
 }
 
 pub fn parse_wasm_extension_version(

crates/extension/src/extension_builder.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
 };
-use anyhow::{Context as _, Result, bail};
+use anyhow::{Context as _, Result, bail, ensure};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use futures::io::BufReader;
@@ -12,6 +12,7 @@ use std::{
     env, fs, mem,
     path::{Path, PathBuf},
     process::Stdio,
+    str::FromStr,
     sync::Arc,
 };
 use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
@@ -97,6 +98,23 @@ impl ExtensionBuilder {
             log::info!("compiled Rust extension {}", extension_dir.display());
         }
 
+        let debug_adapters_dir = extension_dir.join("debug_adapter_schemas");
+        if !extension_manifest.debug_adapters.is_empty() {
+            ensure!(
+                debug_adapters_dir.exists(),
+                "Expected debug adapter schemas directory to exist"
+            );
+        }
+        for debug_adapter_name in &extension_manifest.debug_adapters {
+            let debug_adapter_schema_path = debug_adapters_dir.join(debug_adapter_name.as_ref());
+            let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path)
+                .with_context(|| {
+                    format!("failed to read debug adapter schema for `{debug_adapter_name}`")
+                })?;
+            _ = serde_json::Value::from_str(&debug_adapter_schema).with_context(|| {
+                format!("Debug adapter schema for `{debug_adapter_name}` is not a valid JSON")
+            })?;
+        }
         for (grammar_name, grammar_metadata) in &extension_manifest.grammars {
             let snake_cased_grammar_name = grammar_name.to_snake_case();
             if grammar_name.as_ref() != snake_cased_grammar_name.as_str() {

crates/extension/src/extension_host_proxy.rs 🔗

@@ -412,6 +412,7 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
 
 pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
     fn register_debug_adapter(&self, extension: Arc<dyn Extension>, debug_adapter_name: Arc<str>);
+    fn register_debug_locator(&self, extension: Arc<dyn Extension>, locator_name: Arc<str>);
 }
 
 impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
@@ -422,4 +423,12 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
 
         proxy.register_debug_adapter(extension, debug_adapter_name)
     }
+
+    fn register_debug_locator(&self, extension: Arc<dyn Extension>, locator_name: Arc<str>) {
+        let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else {
+            return;
+        };
+
+        proxy.register_debug_locator(extension, locator_name)
+    }
 }

crates/extension/src/extension_manifest.rs 🔗

@@ -89,6 +89,8 @@ pub struct ExtensionManifest {
     pub capabilities: Vec<ExtensionCapability>,
     #[serde(default)]
     pub debug_adapters: Vec<Arc<str>>,
+    #[serde(default)]
+    pub debug_locators: Vec<Arc<str>>,
 }
 
 impl ExtensionManifest {
@@ -277,6 +279,7 @@ fn manifest_from_old_manifest(
         snippets: None,
         capabilities: Vec::new(),
         debug_adapters: vec![],
+        debug_locators: vec![],
     }
 }
 
@@ -305,6 +308,7 @@ mod tests {
             snippets: None,
             capabilities: vec![],
             debug_adapters: Default::default(),
+            debug_locators: Default::default(),
         }
     }
 

crates/extension/src/types/dap.rs 🔗

@@ -2,4 +2,7 @@ pub use dap::{
     StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
     adapters::{DebugAdapterBinary, DebugTaskDefinition, TcpArguments},
 };
-pub use task::{AttachRequest, DebugRequest, LaunchRequest, TcpArgumentsTemplate};
+pub use task::{
+    AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, LaunchRequest,
+    TaskTemplate as BuildTaskTemplate, TcpArgumentsTemplate,
+};

crates/extension_api/src/extension_api.rs 🔗

@@ -20,8 +20,8 @@ pub use wit::{
     make_file_executable,
     zed::extension::context_server::ContextServerConfiguration,
     zed::extension::dap::{
-        DebugAdapterBinary, DebugTaskDefinition, StartDebuggingRequestArguments,
-        StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
+        DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments,
+        StartDebuggingRequestArgumentsRequest, TaskTemplate, TcpArguments, TcpArgumentsTemplate,
         resolve_tcp_template,
     },
     zed::extension::github::{
@@ -204,8 +204,35 @@ pub trait Extension: Send + Sync {
         Err("`get_dap_binary` not implemented".to_string())
     }
 
-    fn dap_schema(&mut self) -> Result<serde_json::Value, String> {
-        Err("`dap_schema` not implemented".to_string())
+    fn dap_request_kind(
+        &mut self,
+        _adapter_name: String,
+        _config: serde_json::Value,
+    ) -> Result<StartDebuggingRequestArgumentsRequest, String> {
+        Err("`dap_request_kind` not implemented".to_string())
+    }
+    fn dap_config_to_scenario(
+        &mut self,
+        _adapter_name: DebugConfig,
+        _config: &Worktree,
+    ) -> Result<DebugScenario, String> {
+        Err("`dap_config_to_scenario` not implemented".to_string())
+    }
+    fn dap_locator_create_scenario(
+        &mut self,
+        _locator_name: String,
+        _build_task: TaskTemplate,
+        _resolved_label: String,
+        _debug_adapter_name: String,
+    ) -> Option<DebugScenario> {
+        None
+    }
+    fn run_dap_locator(
+        &mut self,
+        _locator_name: String,
+        _build_task: TaskTemplate,
+    ) -> Result<DebugRequest, String> {
+        Err("`run_dap_locator` not implemented".to_string())
     }
 }
 
@@ -401,8 +428,39 @@ impl wit::Guest for Component {
         extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
     }
 
-    fn dap_schema() -> Result<String, String> {
-        extension().dap_schema().map(|schema| schema.to_string())
+    fn dap_request_kind(
+        adapter_name: String,
+        config: String,
+    ) -> Result<StartDebuggingRequestArgumentsRequest, String> {
+        extension().dap_request_kind(
+            adapter_name,
+            serde_json::from_str(&config).map_err(|e| format!("Failed to parse config: {e}"))?,
+        )
+    }
+    fn dap_config_to_scenario(
+        config: DebugConfig,
+        worktree: &Worktree,
+    ) -> Result<DebugScenario, String> {
+        extension().dap_config_to_scenario(config, worktree)
+    }
+    fn dap_locator_create_scenario(
+        locator_name: String,
+        build_task: TaskTemplate,
+        resolved_label: String,
+        debug_adapter_name: String,
+    ) -> Option<DebugScenario> {
+        extension().dap_locator_create_scenario(
+            locator_name,
+            build_task,
+            resolved_label,
+            debug_adapter_name,
+        )
+    }
+    fn run_dap_locator(
+        locator_name: String,
+        build_task: TaskTemplate,
+    ) -> Result<DebugRequest, String> {
+        extension().run_dap_locator(locator_name, build_task)
     }
 }
 

crates/extension_api/wit/since_v0.6.0/dap.wit 🔗

@@ -32,10 +32,55 @@ interface dap {
         timeout: option<u64>,
     }
 
-    record debug-task-definition {
+    /// Debug Config is the "highest-level" configuration for a debug session.
+    /// It comes from a new session modal UI; thus, it is essentially debug-adapter-agnostic.
+    /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario).
+    record debug-config {
+        /// Name of the debug task
+        label: string,
+        /// The debug adapter to use
+        adapter: string,
+        request: debug-request,
+        stop-on-entry: option<bool>,
+    }
+
+    record task-template {
+        /// Human readable name of the task to display in the UI.
+        label: string,
+        /// Executable command to spawn.
+        command: string,
+        args: list<string>,
+        env: env-vars,
+        cwd: option<string>,
+    }
+
+    /// A task template with substituted task variables.
+    type resolved-task = task-template;
+
+    /// A task template for building a debug target.
+    type build-task-template = task-template;
+
+    variant build-task-definition {
+        by-name(string),
+        template(build-task-definition-template-payload )
+    }
+    record build-task-definition-template-payload {
+        locator-name: option<string>,
+        template: build-task-template
+    }
+
+    /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any
+    /// debug-adapter-specific configuration options).
+    record debug-scenario {
+        /// Unsubstituted label for the task.DebugAdapterBinary
         label: string,
+        /// Name of the Debug Adapter this configuration is intended for.
         adapter: string,
+        /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug.
+        build: option<build-task-definition>,
+        /// JSON-encoded configuration for a given debug adapter.
         config: string,
+        /// TCP connection parameters (if they were specified by user)
         tcp-connection: option<tcp-arguments-template>,
     }
 
@@ -44,16 +89,34 @@ interface dap {
         attach,
     }
 
+    record debug-task-definition {
+        /// Unsubstituted label for the task.DebugAdapterBinary
+        label: string,
+        /// Name of the Debug Adapter this configuration is intended for.
+        adapter: string,
+        /// JSON-encoded configuration for a given debug adapter.
+        config: string,
+        /// TCP connection parameters (if they were specified by user)
+        tcp-connection: option<tcp-arguments-template>,
+    }
+
     record start-debugging-request-arguments {
+        /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter.
+        /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter.
         configuration: string,
         request: start-debugging-request-arguments-request,
     }
 
+    /// The lowest-level representation of a debug session, which specifies:
+    /// - How to start a debug adapter process
+    /// - How to start a debug session with it (using DAP protocol)
+    /// for a given debug scenario.
     record debug-adapter-binary {
         command: option<string>,
         arguments: list<string>,
         envs: env-vars,
         cwd: option<string>,
+        /// Zed will use TCP transport if `connection` is specified.
         connection: option<tcp-arguments>,
         request-args: start-debugging-request-arguments
     }

crates/extension_api/wit/since_v0.6.0/extension.wit 🔗

@@ -11,7 +11,7 @@ world extension {
 
     use common.{env-vars, range};
     use context-server.{context-server-configuration};
-    use dap.{debug-adapter-binary, debug-task-definition, debug-request};
+    use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request};
     use lsp.{completion, symbol};
     use process.{command};
     use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
@@ -159,6 +159,9 @@ world extension {
 
     /// Returns a configured debug adapter binary for a given debug task.
     export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
-    /// Get a debug adapter's configuration schema
-    export dap-schema: func() -> result<string, string>;
+    /// Returns the kind of a debug scenario (launch or attach).
+    export dap-request-kind: func(adapter-name: string, config: string) -> result<start-debugging-request-arguments-request, string>;
+    export dap-config-to-scenario: func(config: debug-config, worktree: borrow<worktree>) -> result<debug-scenario, string>;
+    export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option<debug-scenario>;
+    export run-dap-locator: func(locator-name: string, config: resolved-task) -> result<debug-request, string>;
 }

crates/extension_host/src/extension_host.rs 🔗

@@ -1348,6 +1348,11 @@ impl ExtensionStore {
                         this.proxy
                             .register_debug_adapter(extension.clone(), debug_adapter.clone());
                     }
+
+                    for debug_adapter in &manifest.debug_adapters {
+                        this.proxy
+                            .register_debug_locator(extension.clone(), debug_adapter.clone());
+                    }
                 }
 
                 this.wasm_extensions.extend(wasm_extensions);

crates/extension_host/src/extension_store_test.rs 🔗

@@ -163,6 +163,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                         snippets: None,
                         capabilities: Vec::new(),
                         debug_adapters: Default::default(),
+                        debug_locators: Default::default(),
                     }),
                     dev: false,
                 },
@@ -193,6 +194,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                         snippets: None,
                         capabilities: Vec::new(),
                         debug_adapters: Default::default(),
+                        debug_locators: Default::default(),
                     }),
                     dev: false,
                 },
@@ -368,6 +370,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                 snippets: None,
                 capabilities: Vec::new(),
                 debug_adapters: Default::default(),
+                debug_locators: Default::default(),
             }),
             dev: false,
         },

crates/extension_host/src/wasm_host.rs 🔗

@@ -3,6 +3,7 @@ pub mod wit;
 use crate::ExtensionManifest;
 use anyhow::{Context as _, Result, anyhow, bail};
 use async_trait::async_trait;
+use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
 use extension::{
     CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
     DebugTaskDefinition, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate, SlashCommand,
@@ -32,6 +33,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig};
 use wasmtime::{
     CacheStore, Engine, Store,
     component::{Component, ResourceTable},
@@ -399,14 +401,76 @@ impl extension::Extension for WasmExtension {
         })
         .await
     }
+    async fn dap_request_kind(
+        &self,
+        dap_name: Arc<str>,
+        config: serde_json::Value,
+    ) -> Result<StartDebuggingRequestArgumentsRequest> {
+        self.call(|extension, store| {
+            async move {
+                let kind = extension
+                    .call_dap_request_kind(store, dap_name, config)
+                    .await?
+                    .map_err(|err| store.data().extension_error(err))?;
+                Ok(kind.into())
+            }
+            .boxed()
+        })
+        .await
+    }
 
-    async fn get_dap_schema(&self) -> Result<serde_json::Value> {
+    async fn dap_config_to_scenario(
+        &self,
+        config: ZedDebugConfig,
+        worktree: Arc<dyn WorktreeDelegate>,
+    ) -> Result<DebugScenario> {
+        self.call(|extension, store| {
+            async move {
+                let resource = store.data_mut().table().push(worktree)?;
+                let kind = extension
+                    .call_dap_config_to_scenario(store, config, resource)
+                    .await?
+                    .map_err(|err| store.data().extension_error(err))?;
+                Ok(kind)
+            }
+            .boxed()
+        })
+        .await
+    }
+
+    async fn dap_locator_create_scenario(
+        &self,
+        locator_name: String,
+        build_config_template: TaskTemplate,
+        resolved_label: String,
+        debug_adapter_name: String,
+    ) -> Result<Option<DebugScenario>> {
         self.call(|extension, store| {
             async move {
                 extension
-                    .call_dap_schema(store)
+                    .call_dap_locator_create_scenario(
+                        store,
+                        locator_name,
+                        build_config_template,
+                        resolved_label,
+                        debug_adapter_name,
+                    )
                     .await
-                    .and_then(|schema| serde_json::to_value(schema).map_err(|err| err.to_string()))
+            }
+            .boxed()
+        })
+        .await
+    }
+    async fn run_dap_locator(
+        &self,
+        locator_name: String,
+        config: SpawnInTerminal,
+    ) -> Result<DebugRequest> {
+        self.call(|extension, store| {
+            async move {
+                extension
+                    .call_run_dap_locator(store, locator_name, config)
+                    .await?
                     .map_err(|err| store.data().extension_error(err))
             }
             .boxed()

crates/extension_host/src/wasm_host/wit.rs 🔗

@@ -7,10 +7,14 @@ mod since_v0_3_0;
 mod since_v0_4_0;
 mod since_v0_5_0;
 mod since_v0_6_0;
+use dap::DebugRequest;
 use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate};
 use language::LanguageName;
 use lsp::LanguageServerName;
 use release_channel::ReleaseChannel;
+use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig};
+
+use crate::wasm_host::wit::since_v0_6_0::dap::StartDebuggingRequestArgumentsRequest;
 
 use super::{WasmState, wasm_engine};
 use anyhow::{Context as _, Result, anyhow};
@@ -922,18 +926,88 @@ impl Extension {
             _ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"),
         }
     }
+    pub async fn call_dap_request_kind(
+        &self,
+        store: &mut Store<WasmState>,
+        adapter_name: Arc<str>,
+        config: serde_json::Value,
+    ) -> Result<Result<StartDebuggingRequestArgumentsRequest, String>> {
+        match self {
+            Extension::V0_6_0(ext) => {
+                let config =
+                    serde_json::to_string(&config).context("Adapter config is not a valid JSON")?;
+                let dap_binary = ext
+                    .call_dap_request_kind(store, &adapter_name, &config)
+                    .await?
+                    .map_err(|e| anyhow!("{e:?}"))?;
 
-    pub async fn call_dap_schema(&self, store: &mut Store<WasmState>) -> Result<String, String> {
+                Ok(Ok(dap_binary))
+            }
+            _ => anyhow::bail!("`dap_request_kind` not available prior to v0.6.0"),
+        }
+    }
+    pub async fn call_dap_config_to_scenario(
+        &self,
+        store: &mut Store<WasmState>,
+        config: ZedDebugConfig,
+        resource: Resource<Arc<dyn WorktreeDelegate>>,
+    ) -> Result<Result<DebugScenario, String>> {
         match self {
             Extension::V0_6_0(ext) => {
-                let schema = ext
-                    .call_dap_schema(store)
-                    .await
-                    .map_err(|err| err.to_string())?;
+                let config = config.into();
+                let dap_binary = ext
+                    .call_dap_config_to_scenario(store, &config, resource)
+                    .await?
+                    .map_err(|e| anyhow!("{e:?}"))?;
+
+                Ok(Ok(dap_binary.try_into()?))
+            }
+            _ => anyhow::bail!("`dap_config_to_scenario` not available prior to v0.6.0"),
+        }
+    }
+    pub async fn call_dap_locator_create_scenario(
+        &self,
+        store: &mut Store<WasmState>,
+        locator_name: String,
+        build_config_template: TaskTemplate,
+        resolved_label: String,
+        debug_adapter_name: String,
+    ) -> Result<Option<DebugScenario>> {
+        match self {
+            Extension::V0_6_0(ext) => {
+                let build_config_template = build_config_template.into();
+                let dap_binary = ext
+                    .call_dap_locator_create_scenario(
+                        store,
+                        &locator_name,
+                        &build_config_template,
+                        &resolved_label,
+                        &debug_adapter_name,
+                    )
+                    .await?;
+
+                Ok(dap_binary.map(TryInto::try_into).transpose()?)
+            }
+            _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"),
+        }
+    }
+    pub async fn call_run_dap_locator(
+        &self,
+        store: &mut Store<WasmState>,
+        locator_name: String,
+        resolved_build_task: SpawnInTerminal,
+    ) -> Result<Result<DebugRequest, String>> {
+        match self {
+            Extension::V0_6_0(ext) => {
+                let build_config_template = resolved_build_task.into();
+                let dap_request = ext
+                    .call_run_dap_locator(store, &locator_name, &build_config_template)
+                    .await?
+                    .map_err(|e| anyhow!("{e:?}"))?;
 
-                schema
+                Ok(Ok(dap_request.into()))
             }
-            _ => Err("`get_dap_binary` not available prior to v0.6.0".to_string()),
+            _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"),
         }
     }
 }

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -1,7 +1,7 @@
 use crate::wasm_host::wit::since_v0_6_0::{
     dap::{
-        StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArguments,
-        TcpArgumentsTemplate,
+        AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest,
+        StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate,
     },
     slash_command::SlashCommandOutputSection,
 };
@@ -18,6 +18,7 @@ use extension::{
 };
 use futures::{AsyncReadExt, lock::Mutex};
 use futures::{FutureExt as _, io::BufReader};
+use gpui::SharedString;
 use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
 use project::project_settings::ProjectSettings;
 use semantic_version::SemanticVersion;
@@ -25,8 +26,10 @@ use std::{
     env,
     net::Ipv4Addr,
     path::{Path, PathBuf},
+    str::FromStr,
     sync::{Arc, OnceLock},
 };
+use task::{SpawnInTerminal, ZedDebugConfig};
 use util::{archive::extract_zip, maybe};
 use wasmtime::component::{Linker, Resource};
 
@@ -119,6 +122,16 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
     }
 }
 
+impl From<TcpArgumentsTemplate> for extension::TcpArgumentsTemplate {
+    fn from(value: TcpArgumentsTemplate) -> Self {
+        Self {
+            host: value.host.map(Ipv4Addr::from_bits),
+            port: value.port,
+            timeout: value.timeout,
+        }
+    }
+}
+
 impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
     type Error = anyhow::Error;
     fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
@@ -131,6 +144,71 @@ impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
     }
 }
 
+impl From<task::DebugRequest> for DebugRequest {
+    fn from(value: task::DebugRequest) -> Self {
+        match value {
+            task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
+            task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
+        }
+    }
+}
+
+impl From<DebugRequest> for task::DebugRequest {
+    fn from(value: DebugRequest) -> Self {
+        match value {
+            DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
+            DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
+        }
+    }
+}
+
+impl From<task::LaunchRequest> for LaunchRequest {
+    fn from(value: task::LaunchRequest) -> Self {
+        Self {
+            program: value.program,
+            cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()),
+            args: value.args,
+            envs: value.env.into_iter().collect(),
+        }
+    }
+}
+
+impl From<task::AttachRequest> for AttachRequest {
+    fn from(value: task::AttachRequest) -> Self {
+        Self {
+            process_id: value.process_id,
+        }
+    }
+}
+
+impl From<LaunchRequest> for task::LaunchRequest {
+    fn from(value: LaunchRequest) -> Self {
+        Self {
+            program: value.program,
+            cwd: value.cwd.map(|p| p.into()),
+            args: value.args,
+            env: value.envs.into_iter().collect(),
+        }
+    }
+}
+impl From<AttachRequest> for task::AttachRequest {
+    fn from(value: AttachRequest) -> Self {
+        Self {
+            process_id: value.process_id,
+        }
+    }
+}
+
+impl From<ZedDebugConfig> for DebugConfig {
+    fn from(value: ZedDebugConfig) -> Self {
+        Self {
+            label: value.label.into(),
+            adapter: value.adapter.into(),
+            request: value.request.into(),
+            stop_on_entry: value.stop_on_entry,
+        }
+    }
+}
 impl TryFrom<DebugAdapterBinary> for extension::DebugAdapterBinary {
     type Error = anyhow::Error;
     fn try_from(value: DebugAdapterBinary) -> Result<Self, Self::Error> {
@@ -145,6 +223,94 @@ impl TryFrom<DebugAdapterBinary> for extension::DebugAdapterBinary {
     }
 }
 
+impl From<BuildTaskDefinition> for extension::BuildTaskDefinition {
+    fn from(value: BuildTaskDefinition) -> Self {
+        match value {
+            BuildTaskDefinition::ByName(name) => Self::ByName(name.into()),
+            BuildTaskDefinition::Template(build_task_template) => Self::Template {
+                task_template: build_task_template.template.into(),
+                locator_name: build_task_template.locator_name.map(SharedString::from),
+            },
+        }
+    }
+}
+
+impl From<extension::BuildTaskDefinition> for BuildTaskDefinition {
+    fn from(value: extension::BuildTaskDefinition) -> Self {
+        match value {
+            extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()),
+            extension::BuildTaskDefinition::Template {
+                task_template,
+                locator_name,
+            } => Self::Template(BuildTaskDefinitionTemplatePayload {
+                template: task_template.into(),
+                locator_name: locator_name.map(String::from),
+            }),
+        }
+    }
+}
+impl From<BuildTaskTemplate> for extension::BuildTaskTemplate {
+    fn from(value: BuildTaskTemplate) -> Self {
+        Self {
+            label: value.label,
+            command: value.command,
+            args: value.args,
+            env: value.env.into_iter().collect(),
+            cwd: value.cwd,
+            ..Default::default()
+        }
+    }
+}
+impl From<extension::BuildTaskTemplate> for BuildTaskTemplate {
+    fn from(value: extension::BuildTaskTemplate) -> Self {
+        Self {
+            label: value.label,
+            command: value.command,
+            args: value.args,
+            env: value.env.into_iter().collect(),
+            cwd: value.cwd,
+        }
+    }
+}
+
+impl TryFrom<DebugScenario> for extension::DebugScenario {
+    type Error = anyhow::Error;
+
+    fn try_from(value: DebugScenario) -> std::result::Result<Self, Self::Error> {
+        Ok(Self {
+            adapter: value.adapter.into(),
+            label: value.label.into(),
+            build: value.build.map(Into::into),
+            config: serde_json::Value::from_str(&value.config)?,
+            tcp_connection: value.tcp_connection.map(Into::into),
+        })
+    }
+}
+
+impl From<extension::DebugScenario> for DebugScenario {
+    fn from(value: extension::DebugScenario) -> Self {
+        Self {
+            adapter: value.adapter.into(),
+            label: value.label.into(),
+            build: value.build.map(Into::into),
+            config: value.config.to_string(),
+            tcp_connection: value.tcp_connection.map(Into::into),
+        }
+    }
+}
+
+impl From<SpawnInTerminal> for ResolvedTask {
+    fn from(value: SpawnInTerminal) -> Self {
+        Self {
+            label: value.label,
+            command: value.command,
+            args: value.args,
+            env: value.env.into_iter().collect(),
+            cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()),
+        }
+    }
+}
+
 impl From<CodeLabel> for extension::CodeLabel {
     fn from(value: CodeLabel) -> Self {
         Self {