debugger: Remove fake adapter and un-gate GDB (#27557)

Piotr Osiewicz , Anthony Eid , and Anthony created

This is a clean-up PR in anticipation of introduction of Debugger
Registry. I wanna get rid of DebugAdapterKind (or rather, it being an
enum).
Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>

Change summary

Cargo.lock                                                    |   8 
crates/assistant_eval/Cargo.toml                              |   1 
crates/assistant_eval/src/headless_assistant.rs               |   2 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |   4 
crates/collab/src/tests/test_server.rs                        |   4 
crates/dap/Cargo.toml                                         |   5 
crates/dap/src/adapters.rs                                    |  39 
crates/dap/src/client.rs                                      |   5 
crates/dap/src/dap.rs                                         |  17 
crates/dap/src/lib.rs                                         |  38 
crates/dap/src/registry.rs                                    |  39 
crates/dap/src/transport.rs                                   |   8 
crates/dap_adapters/Cargo.toml                                |   1 
crates/dap_adapters/src/custom.rs                             |  84 
crates/dap_adapters/src/dap_adapters.rs                       |  75 
crates/dap_adapters/src/gdb.rs                                |  29 
crates/dap_adapters/src/go.rs                                 |  55 
crates/dap_adapters/src/javascript.rs                         | 101 
crates/dap_adapters/src/lldb.rs                               |  57 
crates/dap_adapters/src/php.rs                                |  61 
crates/dap_adapters/src/python.rs                             |  71 
crates/debugger_ui/Cargo.toml                                 |   4 
crates/debugger_ui/src/attach_modal.rs                        | 117 
crates/debugger_ui/src/debugger_panel.rs                      |   7 
crates/debugger_ui/src/debugger_ui.rs                         |   0 
crates/debugger_ui/src/session.rs                             |   6 
crates/debugger_ui/src/session/inert.rs                       | 129 
crates/debugger_ui/src/tests/attach_modal.rs                  |  57 
crates/debugger_ui/src/tests/console.rs                       |   7 
crates/debugger_ui/src/tests/debugger_panel.rs                |  90 
crates/debugger_ui/src/tests/module_list.rs                   |  19 
crates/debugger_ui/src/tests/stack_frame_list.rs              |  19 
crates/debugger_ui/src/tests/variable_list.rs                 |  31 
crates/evals/Cargo.toml                                       |   1 
crates/evals/src/eval.rs                                      |   3 
crates/project/Cargo.toml                                     |   1 
crates/project/src/debugger.rs                                |   2 
crates/project/src/debugger/dap_store.rs                      | 195 +
crates/project/src/debugger/session.rs                        | 437 +++-
crates/project/src/project.rs                                 |  59 
crates/recent_projects/src/ssh_connections.rs                 |   1 
crates/remote_server/Cargo.toml                               |   2 
crates/remote_server/src/headless_project.rs                  |   4 
crates/remote_server/src/remote_editing_tests.rs              |   3 
crates/remote_server/src/unix.rs                              |   3 
crates/task/src/debug_format.rs                               | 216 +-
crates/task/src/lib.rs                                        |  16 
crates/task/src/task_template.rs                              |  27 
crates/tasks_ui/src/modal.rs                                  |   7 
crates/workspace/Cargo.toml                                   |   2 
crates/workspace/src/workspace.rs                             |   6 
crates/zed/Cargo.toml                                         |   3 
crates/zed/src/main.rs                                        |   3 
53 files changed, 1,286 insertions(+), 895 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -581,6 +581,7 @@ dependencies = [
  "client",
  "collections",
  "context_server",
+ "dap",
  "env_logger 0.11.7",
  "fs",
  "futures 0.3.31",
@@ -3875,6 +3876,7 @@ dependencies = [
  "node_runtime",
  "parking_lot",
  "paths",
+ "regex",
  "schemars",
  "serde",
  "serde_json",
@@ -3908,7 +3910,6 @@ dependencies = [
  "regex",
  "serde",
  "serde_json",
- "sysinfo",
  "task",
  "util",
 ]
@@ -4642,6 +4643,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "dap",
  "env_logger 0.11.7",
  "feature_flags",
  "fs",
@@ -11349,6 +11351,7 @@ dependencies = [
  "clap",
  "client",
  "clock",
+ "dap",
  "env_logger 0.11.7",
  "extension",
  "extension_host",
@@ -16908,6 +16911,7 @@ dependencies = [
  "clock",
  "collections",
  "component",
+ "dap",
  "db",
  "derive_more",
  "env_logger 0.11.7",
@@ -17284,6 +17288,8 @@ dependencies = [
  "command_palette_hooks",
  "component_preview",
  "copilot",
+ "dap",
+ "dap_adapters",
  "db",
  "debugger_tools",
  "debugger_ui",

crates/assistant_eval/Cargo.toml 🔗

@@ -21,6 +21,7 @@ clap.workspace = true
 client.workspace = true
 collections.workspace = true
 context_server.workspace = true
+dap.workspace = true
 env_logger.workspace = true
 fs.workspace = true
 futures.workspace = true

crates/assistant_eval/src/headless_assistant.rs 🔗

@@ -3,6 +3,7 @@ use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
 use assistant_tool::ToolWorkingSet;
 use client::{Client, UserStore};
 use collections::HashMap;
+use dap::DapRegistry;
 use futures::StreamExt;
 use gpui::{prelude::*, App, AsyncApp, Entity, SemanticVersion, Subscription, Task};
 use language::LanguageRegistry;
@@ -50,6 +51,7 @@ impl HeadlessAssistant {
             app_state.node_runtime.clone(),
             app_state.user_store.clone(),
             app_state.languages.clone(),
+            Arc::new(DapRegistry::default()),
             app_state.fs.clone(),
             env,
             cx,

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -1,6 +1,7 @@
 use crate::tests::TestServer;
 use call::ActiveCall;
 use collections::{HashMap, HashSet};
+use dap::DapRegistry;
 use extension::ExtensionHostProxy;
 use fs::{FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
@@ -85,6 +86,7 @@ async fn test_sharing_an_ssh_remote_project(
                 http_client: remote_http_client,
                 node_runtime: node,
                 languages,
+                debug_adapters: Arc::new(DapRegistry::fake()),
                 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
             },
             cx,
@@ -252,6 +254,7 @@ async fn test_ssh_collaboration_git_branches(
                 http_client: remote_http_client,
                 node_runtime: node,
                 languages,
+                debug_adapters: Arc::new(DapRegistry::fake()),
                 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
             },
             cx,
@@ -451,6 +454,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
                 http_client: remote_http_client,
                 node_runtime: NodeRuntime::unavailable(),
                 languages,
+                debug_adapters: Arc::new(DapRegistry::fake()),
                 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
             },
             cx,

crates/collab/src/tests/test_server.rs 🔗

@@ -14,6 +14,7 @@ use client::{
 use clock::FakeSystemClock;
 use collab_ui::channel_view::ChannelView;
 use collections::{HashMap, HashSet};
+use dap::DapRegistry;
 use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
 use git::GitHostingProviderRegistry;
@@ -277,12 +278,14 @@ impl TestServer {
         let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
         let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
+        let debug_adapters = Arc::new(DapRegistry::default());
         let session = cx.new(|cx| AppSession::new(Session::test(), cx));
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
             workspace_store,
             languages: language_registry,
+            debug_adapters,
             fs: fs.clone(),
             build_window_options: |_, _| Default::default(),
             node_runtime: NodeRuntime::unavailable(),
@@ -795,6 +798,7 @@ impl TestClient {
                 self.app_state.node_runtime.clone(),
                 self.app_state.user_store.clone(),
                 self.app_state.languages.clone(),
+                self.app_state.debug_adapters.clone(),
                 self.app_state.fs.clone(),
                 None,
                 cx,

crates/dap/Cargo.toml 🔗

@@ -8,6 +8,10 @@ license = "GPL-3.0-or-later"
 [lints]
 workspace = true
 
+[lib]
+path = "src/dap.rs"
+doctest = false
+
 [features]
 test-support = [
     "gpui/test-support",
@@ -35,6 +39,7 @@ log.workspace = true
 node_runtime.workspace = true
 parking_lot.workspace = true
 paths.workspace = true
+regex.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/dap/src/adapters.rs 🔗

@@ -13,15 +13,16 @@ use serde_json::Value;
 use settings::WorktreeId;
 use smol::{self, fs::File, lock::Mutex};
 use std::{
+    borrow::Borrow,
     collections::{HashMap, HashSet},
     ffi::{OsStr, OsString},
     fmt::Debug,
     net::Ipv4Addr,
     ops::Deref,
-    path::{Path, PathBuf},
-    sync::Arc,
+    path::PathBuf,
+    sync::{Arc, LazyLock},
 };
-use task::DebugAdapterConfig;
+use task::{DebugAdapterConfig, DebugTaskDefinition};
 use util::ResultExt;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -46,7 +47,7 @@ pub trait DapDelegate {
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
-pub struct DebugAdapterName(pub Arc<str>);
+pub struct DebugAdapterName(pub SharedString);
 
 impl Deref for DebugAdapterName {
     type Target = str;
@@ -62,9 +63,9 @@ impl AsRef<str> for DebugAdapterName {
     }
 }
 
-impl AsRef<Path> for DebugAdapterName {
-    fn as_ref(&self) -> &Path {
-        Path::new(&*self.0)
+impl Borrow<str> for DebugAdapterName {
+    fn borrow(&self) -> &str {
+        &self.0
     }
 }
 
@@ -76,7 +77,7 @@ impl std::fmt::Display for DebugAdapterName {
 
 impl From<DebugAdapterName> for SharedString {
     fn from(name: DebugAdapterName) -> Self {
-        SharedString::from(name.0)
+        name.0
     }
 }
 
@@ -123,7 +124,7 @@ pub async fn download_adapter_from_github(
     file_type: DownloadedFileType,
     delegate: &dyn DapDelegate,
 ) -> Result<PathBuf> {
-    let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
+    let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
     let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
     let fs = delegate.fs();
 
@@ -288,15 +289,21 @@ pub trait DebugAdapter: 'static + Send + Sync {
     ) -> Result<DebugAdapterBinary>;
 
     /// Should return base configuration to make the debug adapter work
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value;
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value;
+
+    fn attach_processes_filter(&self) -> regex::Regex {
+        EMPTY_REGEX.clone()
+    }
 }
 
+static EMPTY_REGEX: LazyLock<regex::Regex> =
+    LazyLock::new(|| regex::Regex::new("").expect("Regex compilation to succeed"));
 #[cfg(any(test, feature = "test-support"))]
 pub struct FakeAdapter {}
 
 #[cfg(any(test, feature = "test-support"))]
 impl FakeAdapter {
-    const ADAPTER_NAME: &'static str = "fake-adapter";
+    pub const ADAPTER_NAME: &'static str = "fake-adapter";
 
     pub fn new() -> Self {
         Self {}
@@ -351,13 +358,13 @@ impl DebugAdapter for FakeAdapter {
         unimplemented!("get installed binary");
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
         use serde_json::json;
         use task::DebugRequestType;
 
         json!({
             "request": match config.request {
-                DebugRequestType::Launch => "launch",
+                DebugRequestType::Launch(_) => "launch",
                 DebugRequestType::Attach(_) => "attach",
             },
             "process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
@@ -367,4 +374,10 @@ impl DebugAdapter for FakeAdapter {
             },
         })
     }
+
+    fn attach_processes_filter(&self) -> regex::Regex {
+        static REGEX: LazyLock<regex::Regex> =
+            LazyLock::new(|| regex::Regex::new("^fake-binary").unwrap());
+        REGEX.clone()
+    }
 }

crates/dap/src/client.rs 🔗

@@ -71,7 +71,6 @@ impl DebugAdapterClient {
         let client_id = this.id;
 
         // start handling events/reverse requests
-
         cx.background_spawn(Self::handle_receive_messages(
             client_id,
             server_rx,
@@ -119,7 +118,6 @@ impl DebugAdapterClient {
                 Ok(message) => message,
                 Err(e) => break Err(e.into()),
             };
-
             match message {
                 Message::Event(ev) => {
                     log::debug!("Client {} received event `{}`", client_id.0, &ev);
@@ -164,7 +162,6 @@ impl DebugAdapterClient {
             command: R::COMMAND.to_string(),
             arguments: Some(serialized_arguments),
         };
-
         self.transport_delegate
             .add_pending_request(sequence_id, callback_tx)
             .await;
@@ -434,7 +431,7 @@ mod tests {
 
         let client = DebugAdapterClient::start(
             crate::client::SessionId(1),
-            DebugAdapterName(Arc::from("test-adapter")),
+            DebugAdapterName("test-adapter".into()),
             DebugAdapterBinary {
                 command: "command".into(),
                 arguments: Default::default(),

crates/dap/src/dap.rs 🔗

@@ -0,0 +1,17 @@
+pub mod adapters;
+pub mod client;
+pub mod debugger_settings;
+pub mod proto_conversions;
+mod registry;
+pub mod transport;
+
+pub use dap_types::*;
+pub use registry::DapRegistry;
+pub use task::{DebugAdapterConfig, DebugRequestType};
+
+pub type ScopeId = u64;
+pub type VariableReference = u64;
+pub type StackFrameId = u64;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use adapters::FakeAdapter;

crates/dap/src/lib.rs 🔗

@@ -1,38 +0,0 @@
-pub mod adapters;
-pub mod client;
-pub mod debugger_settings;
-pub mod proto_conversions;
-pub mod transport;
-
-pub use dap_types::*;
-pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
-
-pub type ScopeId = u64;
-pub type VariableReference = u64;
-pub type StackFrameId = u64;
-
-#[cfg(any(test, feature = "test-support"))]
-pub use adapters::FakeAdapter;
-
-#[cfg(any(test, feature = "test-support"))]
-pub fn test_config(
-    request: DebugRequestType,
-    fail: Option<bool>,
-    caps: Option<Capabilities>,
-) -> DebugAdapterConfig {
-    DebugAdapterConfig {
-        label: "test config".into(),
-        kind: DebugAdapterKind::Fake((
-            fail.unwrap_or_default(),
-            caps.unwrap_or(Capabilities {
-                supports_step_back: Some(false),
-                ..Default::default()
-            }),
-        )),
-        request,
-        program: None,
-        supports_attach: false,
-        cwd: None,
-        initialize_args: None,
-    }
-}

crates/dap/src/registry.rs 🔗

@@ -0,0 +1,39 @@
+use parking_lot::RwLock;
+
+use crate::adapters::{DebugAdapter, DebugAdapterName};
+use std::{collections::BTreeMap, sync::Arc};
+
+#[derive(Default)]
+struct DapRegistryState {
+    adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
+}
+
+#[derive(Default)]
+/// Stores available debug adapters.
+pub struct DapRegistry(Arc<RwLock<DapRegistryState>>);
+
+impl DapRegistry {
+    pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
+        let name = adapter.name();
+        let _previous_value = self.0.write().adapters.insert(name, adapter);
+        debug_assert!(
+            _previous_value.is_none(),
+            "Attempted to insert a new debug adapter when one is already registered"
+        );
+    }
+    pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
+        self.0.read().adapters.get(name).cloned()
+    }
+    pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
+        self.0.read().adapters.keys().cloned().collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn fake() -> Self {
+        use crate::FakeAdapter;
+
+        let register = Self::default();
+        register.add_adapter(Arc::new(FakeAdapter::new()));
+        register
+    }
+}

crates/dap/src/transport.rs 🔗

@@ -261,8 +261,6 @@ impl TransportDelegate {
                     }
                 }
             }
-
-            smol::future::yield_now().await;
         };
 
         log::debug!("Handle adapter log dropped");
@@ -319,8 +317,6 @@ impl TransportDelegate {
                 }
                 Err(error) => break Err(error.into()),
             }
-
-            smol::future::yield_now().await;
         };
 
         log::debug!("Handle adapter input dropped");
@@ -360,8 +356,6 @@ impl TransportDelegate {
                 }
                 Err(e) => break Err(e),
             }
-
-            smol::future::yield_now().await;
         };
 
         drop(client_tx);
@@ -393,8 +387,6 @@ impl TransportDelegate {
                 }
                 Err(error) => break Err(error.into()),
             }
-
-            smol::future::yield_now().await;
         };
 
         log::debug!("Handle adapter error dropped");

crates/dap_adapters/Cargo.toml 🔗

@@ -30,7 +30,6 @@ paths.workspace = true
 regex.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-sysinfo.workspace = true
 task.workspace = true
 util.workspace = true
 

crates/dap_adapters/src/custom.rs 🔗

@@ -1,84 +0,0 @@
-use dap::transport::TcpTransport;
-use gpui::AsyncApp;
-use serde_json::Value;
-use std::{collections::HashMap, ffi::OsString, path::PathBuf};
-use sysinfo::{Pid, Process};
-use task::DebugAdapterConfig;
-
-use crate::*;
-
-pub(crate) struct CustomDebugAdapter {
-    custom_args: CustomArgs,
-}
-
-impl CustomDebugAdapter {
-    const ADAPTER_NAME: &'static str = "custom_dap";
-
-    pub(crate) async fn new(custom_args: CustomArgs) -> Result<Self> {
-        Ok(CustomDebugAdapter { custom_args })
-    }
-
-    pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
-        processes.iter().collect::<Vec<_>>()
-    }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for CustomDebugAdapter {
-    fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
-    }
-
-    async fn get_binary(
-        &self,
-        _: &dyn DapDelegate,
-        config: &DebugAdapterConfig,
-        _: Option<PathBuf>,
-        _: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection
-        {
-            Some(adapters::TcpArguments {
-                host: connection.host(),
-                port: TcpTransport::port(&connection).await?,
-                timeout: connection.timeout,
-            })
-        } else {
-            None
-        };
-        let ret = DebugAdapterBinary {
-            command: self.custom_args.command.clone(),
-            arguments: self
-                .custom_args
-                .args
-                .clone()
-                .map(|args| args.iter().map(OsString::from).collect()),
-            cwd: config.cwd.clone(),
-            envs: self.custom_args.envs.clone(),
-            connection,
-        };
-        Ok(ret)
-    }
-
-    async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
-        bail!("Custom debug adapters don't have latest versions")
-    }
-
-    async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> {
-        bail!("Custom debug adapters cannot be installed")
-    }
-
-    async fn get_installed_binary(
-        &self,
-        _: &dyn DapDelegate,
-        _: &DebugAdapterConfig,
-        _: Option<PathBuf>,
-        _: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        bail!("Custom debug adapters cannot be installed")
-    }
-
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        json!({"program": config.program})
-    }
-}

crates/dap_adapters/src/dap_adapters.rs 🔗

@@ -1,5 +1,3 @@
-mod custom;
-#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
 mod gdb;
 mod go;
 mod javascript;
@@ -7,16 +5,17 @@ mod lldb;
 mod php;
 mod python;
 
-use std::{collections::HashMap, sync::Arc};
+use std::{net::Ipv4Addr, sync::Arc};
 
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use custom::CustomDebugAdapter;
-use dap::adapters::{
-    self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
-    GithubRepo,
+use dap::{
+    adapters::{
+        self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
+        GithubRepo,
+    },
+    DapRegistry,
 };
-#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
 use gdb::GdbDebugAdapter;
 use go::GoDebugAdapter;
 use javascript::JsDebugAdapter;
@@ -24,44 +23,28 @@ use lldb::LldbDebugAdapter;
 use php::PhpDebugAdapter;
 use python::PythonDebugAdapter;
 use serde_json::{json, Value};
-use sysinfo::{Pid, Process};
-use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
+use task::{DebugAdapterConfig, TCPHost};
 
-pub async fn build_adapter(kind: &DebugAdapterKind) -> Result<Arc<dyn DebugAdapter>> {
-    match kind {
-        DebugAdapterKind::Custom(start_args) => {
-            Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?))
-        }
-        DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)),
-        DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)),
-        DebugAdapterKind::Javascript(host) => {
-            Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?))
-        }
-        DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())),
-        DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)),
-        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
-        DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())),
-        #[cfg(any(test, feature = "test-support"))]
-        DebugAdapterKind::Fake(_) => Ok(Arc::new(dap::adapters::FakeAdapter::new())),
-        #[cfg(not(any(test, feature = "test-support")))]
-        #[allow(unreachable_patterns)]
-        _ => unreachable!("Fake variant only exists with test-support feature"),
-    }
+pub fn init(registry: Arc<DapRegistry>) {
+    registry.add_adapter(Arc::from(PythonDebugAdapter));
+    registry.add_adapter(Arc::from(PhpDebugAdapter));
+    registry.add_adapter(Arc::from(JsDebugAdapter::default()));
+    registry.add_adapter(Arc::from(LldbDebugAdapter));
+    registry.add_adapter(Arc::from(GoDebugAdapter));
+    registry.add_adapter(Arc::from(GdbDebugAdapter));
 }
 
-pub fn attach_processes<'a>(
-    kind: &DebugAdapterKind,
-    processes: &'a HashMap<Pid, Process>,
-) -> Vec<(&'a Pid, &'a Process)> {
-    match kind {
-        #[cfg(any(test, feature = "test-support"))]
-        DebugAdapterKind::Fake(_) => processes
-            .iter()
-            .filter(|(pid, _)| pid.as_u32() == std::process::id())
-            .collect::<Vec<_>>(),
-        DebugAdapterKind::Custom(_) => CustomDebugAdapter::attach_processes(processes),
-        DebugAdapterKind::Javascript(_) => JsDebugAdapter::attach_processes(processes),
-        DebugAdapterKind::Lldb => LldbDebugAdapter::attach_processes(processes),
-        _ => processes.iter().collect::<Vec<_>>(),
-    }
+pub(crate) async fn configure_tcp_connection(
+    tcp_connection: TCPHost,
+) -> Result<(Ipv4Addr, u16, Option<u64>)> {
+    let host = tcp_connection.host();
+    let timeout = tcp_connection.timeout;
+
+    let port = if let Some(port) = tcp_connection.port {
+        port
+    } else {
+        dap::transport::TcpTransport::port(&tcp_connection).await?
+    };
+
+    Ok((host, port, timeout))
 }

crates/dap_adapters/src/gdb.rs 🔗

@@ -1,20 +1,17 @@
 use std::ffi::OsStr;
 
-use anyhow::Result;
+use anyhow::{bail, Result};
 use async_trait::async_trait;
 use gpui::AsyncApp;
-use task::DebugAdapterConfig;
+use task::{DebugAdapterConfig, DebugTaskDefinition};
 
 use crate::*;
 
-pub(crate) struct GdbDebugAdapter {}
+#[derive(Default)]
+pub(crate) struct GdbDebugAdapter;
 
 impl GdbDebugAdapter {
-    const ADAPTER_NAME: &'static str = "gdb";
-
-    pub(crate) fn new() -> Self {
-        GdbDebugAdapter {}
-    }
+    const ADAPTER_NAME: &'static str = "GDB";
 }
 
 #[async_trait(?Send)]
@@ -26,7 +23,7 @@ impl DebugAdapter for GdbDebugAdapter {
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,
-        config: &DebugAdapterConfig,
+        _: &DebugAdapterConfig,
         user_installed_path: Option<std::path::PathBuf>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
@@ -34,7 +31,6 @@ impl DebugAdapter for GdbDebugAdapter {
             .filter(|p| p.exists())
             .and_then(|p| p.to_str().map(|s| s.to_string()));
 
-        /* GDB implements DAP natively so just need to  */
         let gdb_path = delegate
             .which(OsStr::new("gdb"))
             .and_then(|p| p.to_str().map(|s| s.to_string()))
@@ -50,7 +46,7 @@ impl DebugAdapter for GdbDebugAdapter {
             command: gdb_path,
             arguments: Some(vec!["-i=dap".into()]),
             envs: None,
-            cwd: config.cwd.clone(),
+            cwd: None,
             connection: None,
         })
     }
@@ -77,7 +73,14 @@ impl DebugAdapter for GdbDebugAdapter {
         unimplemented!("GDB cannot be installed by Zed (yet)")
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        json!({"program": config.program, "cwd": config.cwd})
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        match &config.request {
+            dap::DebugRequestType::Attach(attach_config) => {
+                json!({"pid": attach_config.process_id})
+            }
+            dap::DebugRequestType::Launch(launch_config) => {
+                json!({"program": launch_config.program, "cwd": launch_config.cwd})
+            }
+        }
     }
 }

crates/dap_adapters/src/go.rs 🔗

@@ -1,25 +1,15 @@
-use dap::transport::TcpTransport;
+use anyhow::bail;
 use gpui::AsyncApp;
-use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
+use std::{ffi::OsStr, path::PathBuf};
+use task::DebugTaskDefinition;
 
 use crate::*;
 
-pub(crate) struct GoDebugAdapter {
-    port: u16,
-    host: Ipv4Addr,
-    timeout: Option<u64>,
-}
+#[derive(Default, Debug)]
+pub(crate) struct GoDebugAdapter;
 
 impl GoDebugAdapter {
-    const ADAPTER_NAME: &'static str = "delve";
-
-    pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
-        Ok(GoDebugAdapter {
-            port: TcpTransport::port(host).await?,
-            host: host.host(),
-            timeout: host.timeout,
-        })
-    }
+    const ADAPTER_NAME: &'static str = "Delve";
 }
 
 #[async_trait(?Send)]
@@ -73,28 +63,39 @@ impl DebugAdapter for GoDebugAdapter {
             .and_then(|p| p.to_str().map(|p| p.to_string()))
             .ok_or(anyhow!("Dlv not found in path"))?;
 
+        let Some(tcp_connection) = config.tcp_connection.clone() else {
+            bail!("Go Debug Adapter expects tcp connection arguments to be provided");
+        };
+        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+
         Ok(DebugAdapterBinary {
             command: delve_path,
             arguments: Some(vec![
                 "dap".into(),
                 "--listen".into(),
-                format!("{}:{}", self.host, self.port).into(),
+                format!("{}:{}", host, port).into(),
             ]),
-            cwd: config.cwd.clone(),
+            cwd: None,
             envs: None,
             connection: Some(adapters::TcpArguments {
-                host: self.host,
-                port: self.port,
-                timeout: self.timeout,
+                host,
+                port,
+                timeout,
             }),
         })
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        json!({
-            "program": config.program,
-            "cwd": config.cwd,
-            "subProcess": true,
-        })
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        match &config.request {
+            dap::DebugRequestType::Attach(attach_config) => {
+                json!({
+                    "processId": attach_config.process_id
+                })
+            }
+            dap::DebugRequestType::Launch(launch_config) => json!({
+                "program": launch_config.program,
+                "cwd": launch_config.cwd,
+            }),
+        }
     }
 }

crates/dap_adapters/src/javascript.rs 🔗

@@ -1,39 +1,28 @@
 use adapters::latest_github_release;
-use dap::transport::TcpTransport;
 use gpui::AsyncApp;
 use regex::Regex;
-use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf};
-use sysinfo::{Pid, Process};
-use task::DebugRequestType;
+use std::path::PathBuf;
+use task::{DebugRequestType, DebugTaskDefinition};
 
 use crate::*;
 
+#[derive(Debug)]
 pub(crate) struct JsDebugAdapter {
-    port: u16,
-    host: Ipv4Addr,
-    timeout: Option<u64>,
+    attach_processes: Regex,
 }
 
+impl Default for JsDebugAdapter {
+    fn default() -> Self {
+        Self {
+            attach_processes: Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)")
+                .expect("Regex compilation to succeed"),
+        }
+    }
+}
 impl JsDebugAdapter {
-    const ADAPTER_NAME: &'static str = "vscode-js-debug";
+    const ADAPTER_NAME: &'static str = "JavaScript";
+    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
     const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
-
-    pub(crate) async fn new(host: TCPHost) -> Result<Self> {
-        Ok(JsDebugAdapter {
-            host: host.host(),
-            timeout: host.timeout,
-            port: TcpTransport::port(&host).await?,
-        })
-    }
-
-    pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
-        let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap();
-
-        processes
-            .iter()
-            .filter(|(_, process)| regex.is_match(&process.name().to_string_lossy()))
-            .collect::<Vec<_>>()
-    }
 }
 
 #[async_trait(?Send)]
@@ -47,7 +36,7 @@ impl DebugAdapter for JsDebugAdapter {
         delegate: &dyn DapDelegate,
     ) -> Result<AdapterVersion> {
         let release = latest_github_release(
-            &format!("{}/{}", "microsoft", Self::ADAPTER_NAME),
+            &format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
             true,
             false,
             delegate.http_client(),
@@ -78,7 +67,7 @@ impl DebugAdapter for JsDebugAdapter {
         let adapter_path = if let Some(user_installed_path) = user_installed_path {
             user_installed_path
         } else {
-            let adapter_path = paths::debug_adapters_dir().join(self.name());
+            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 
             let file_name_prefix = format!("{}_", self.name());
 
@@ -89,6 +78,13 @@ impl DebugAdapter for JsDebugAdapter {
             .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
         };
 
+        let Some(tcp_connection) = config.tcp_connection.clone() else {
+            anyhow::bail!(
+                "Javascript Debug Adapter expects tcp connection arguments to be provided"
+            );
+        };
+        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+
         Ok(DebugAdapterBinary {
             command: delegate
                 .node_runtime()
@@ -98,15 +94,15 @@ impl DebugAdapter for JsDebugAdapter {
                 .into_owned(),
             arguments: Some(vec![
                 adapter_path.join(Self::ADAPTER_PATH).into(),
-                self.port.to_string().into(),
-                self.host.to_string().into(),
+                port.to_string().into(),
+                host.to_string().into(),
             ]),
-            cwd: config.cwd.clone(),
+            cwd: None,
             envs: None,
             connection: Some(adapters::TcpArguments {
-                host: self.host,
-                port: self.port,
-                timeout: self.timeout,
+                host,
+                port,
+                timeout,
             }),
         })
     }
@@ -127,22 +123,35 @@ impl DebugAdapter for JsDebugAdapter {
         return Ok(());
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
-            attach_config.process_id
-        } else {
-            None
-        };
-
-        json!({
-            "program": config.program,
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        let mut args = json!({
             "type": "pwa-node",
             "request": match config.request {
-                DebugRequestType::Launch => "launch",
+                DebugRequestType::Launch(_) => "launch",
                 DebugRequestType::Attach(_) => "attach",
             },
-            "processId": pid,
-            "cwd": config.cwd,
-        })
+        });
+        let map = args.as_object_mut().unwrap();
+        match &config.request {
+            DebugRequestType::Attach(attach) => {
+                map.insert("processId".into(), attach.process_id.into());
+            }
+            DebugRequestType::Launch(launch) => {
+                map.insert("program".into(), launch.program.clone().into());
+                map.insert(
+                    "cwd".into(),
+                    launch
+                        .cwd
+                        .as_ref()
+                        .map(|s| s.to_string_lossy().into_owned())
+                        .into(),
+                );
+            }
+        }
+        args
+    }
+
+    fn attach_processes_filter(&self) -> Regex {
+        self.attach_processes.clone()
     }
 }

crates/dap_adapters/src/lldb.rs 🔗

@@ -1,25 +1,17 @@
-use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
+use std::{ffi::OsStr, path::PathBuf};
 
 use anyhow::Result;
 use async_trait::async_trait;
 use gpui::AsyncApp;
-use sysinfo::{Pid, Process};
-use task::{DebugAdapterConfig, DebugRequestType};
+use task::{DebugAdapterConfig, DebugRequestType, DebugTaskDefinition};
 
 use crate::*;
 
-pub(crate) struct LldbDebugAdapter {}
+#[derive(Default)]
+pub(crate) struct LldbDebugAdapter;
 
 impl LldbDebugAdapter {
-    const ADAPTER_NAME: &'static str = "lldb";
-
-    pub(crate) fn new() -> Self {
-        LldbDebugAdapter {}
-    }
-
-    pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
-        processes.iter().collect::<Vec<_>>()
-    }
+    const ADAPTER_NAME: &'static str = "LLDB";
 }
 
 #[async_trait(?Send)]
@@ -31,7 +23,7 @@ impl DebugAdapter for LldbDebugAdapter {
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,
-        config: &DebugAdapterConfig,
+        _: &DebugAdapterConfig,
         user_installed_path: Option<PathBuf>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
@@ -48,7 +40,7 @@ impl DebugAdapter for LldbDebugAdapter {
             command: lldb_dap_path,
             arguments: None,
             envs: None,
-            cwd: config.cwd.clone(),
+            cwd: None,
             connection: None,
         })
     }
@@ -75,21 +67,30 @@ impl DebugAdapter for LldbDebugAdapter {
         unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
-            attach_config.process_id
-        } else {
-            None
-        };
-
-        json!({
-            "program": config.program,
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        let mut args = json!({
             "request": match config.request {
-                DebugRequestType::Launch => "launch",
+                DebugRequestType::Launch(_) => "launch",
                 DebugRequestType::Attach(_) => "attach",
             },
-            "pid": pid,
-            "cwd": config.cwd,
-        })
+        });
+        let map = args.as_object_mut().unwrap();
+        match &config.request {
+            DebugRequestType::Attach(attach) => {
+                map.insert("pid".into(), attach.process_id.into());
+            }
+            DebugRequestType::Launch(launch) => {
+                map.insert("program".into(), launch.program.clone().into());
+                map.insert(
+                    "cwd".into(),
+                    launch
+                        .cwd
+                        .as_ref()
+                        .map(|s| s.to_string_lossy().into_owned())
+                        .into(),
+                );
+            }
+        }
+        args
     }
 }

crates/dap_adapters/src/php.rs 🔗

@@ -1,27 +1,19 @@
 use adapters::latest_github_release;
-use dap::{adapters::TcpArguments, transport::TcpTransport};
+use anyhow::bail;
+use dap::adapters::TcpArguments;
 use gpui::AsyncApp;
-use std::{net::Ipv4Addr, path::PathBuf};
+use std::path::PathBuf;
+use task::DebugTaskDefinition;
 
 use crate::*;
 
-pub(crate) struct PhpDebugAdapter {
-    port: u16,
-    host: Ipv4Addr,
-    timeout: Option<u64>,
-}
+#[derive(Default)]
+pub(crate) struct PhpDebugAdapter;
 
 impl PhpDebugAdapter {
-    const ADAPTER_NAME: &'static str = "vscode-php-debug";
+    const ADAPTER_NAME: &'static str = "PHP";
+    const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
     const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
-
-    pub(crate) async fn new(host: TCPHost) -> Result<Self> {
-        Ok(PhpDebugAdapter {
-            port: TcpTransport::port(&host).await?,
-            host: host.host(),
-            timeout: host.timeout,
-        })
-    }
 }
 
 #[async_trait(?Send)]
@@ -35,7 +27,7 @@ impl DebugAdapter for PhpDebugAdapter {
         delegate: &dyn DapDelegate,
     ) -> Result<AdapterVersion> {
         let release = latest_github_release(
-            &format!("{}/{}", "xdebug", Self::ADAPTER_NAME),
+            &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
             true,
             false,
             delegate.http_client(),
@@ -66,7 +58,7 @@ impl DebugAdapter for PhpDebugAdapter {
         let adapter_path = if let Some(user_installed_path) = user_installed_path {
             user_installed_path
         } else {
-            let adapter_path = paths::debug_adapters_dir().join(self.name());
+            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 
             let file_name_prefix = format!("{}_", self.name());
 
@@ -77,6 +69,11 @@ impl DebugAdapter for PhpDebugAdapter {
             .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
         };
 
+        let Some(tcp_connection) = config.tcp_connection.clone() else {
+            bail!("PHP Debug Adapter expects tcp connection arguments to be provided");
+        };
+        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+
         Ok(DebugAdapterBinary {
             command: delegate
                 .node_runtime()
@@ -86,14 +83,14 @@ impl DebugAdapter for PhpDebugAdapter {
                 .into_owned(),
             arguments: Some(vec![
                 adapter_path.join(Self::ADAPTER_PATH).into(),
-                format!("--server={}", self.port).into(),
+                format!("--server={}", port).into(),
             ]),
             connection: Some(TcpArguments {
-                port: self.port,
-                host: self.host,
-                timeout: self.timeout,
+                port,
+                host,
+                timeout,
             }),
-            cwd: config.cwd.clone(),
+            cwd: None,
             envs: None,
         })
     }
@@ -114,10 +111,18 @@ impl DebugAdapter for PhpDebugAdapter {
         Ok(())
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        json!({
-            "program": config.program,
-            "cwd": config.cwd,
-        })
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        match &config.request {
+            dap::DebugRequestType::Attach(_) => {
+                // php adapter does not support attaching
+                json!({})
+            }
+            dap::DebugRequestType::Launch(launch_config) => {
+                json!({
+                    "program": launch_config.program,
+                    "cwd": launch_config.cwd,
+                })
+            }
+        }
     }
 }

crates/dap_adapters/src/python.rs 🔗

@@ -1,26 +1,18 @@
 use crate::*;
-use dap::transport::TcpTransport;
+use anyhow::bail;
+use dap::DebugRequestType;
 use gpui::AsyncApp;
-use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
+use std::{ffi::OsStr, path::PathBuf};
+use task::DebugTaskDefinition;
 
-pub(crate) struct PythonDebugAdapter {
-    port: u16,
-    host: Ipv4Addr,
-    timeout: Option<u64>,
-}
+#[derive(Default)]
+pub(crate) struct PythonDebugAdapter;
 
 impl PythonDebugAdapter {
-    const ADAPTER_NAME: &'static str = "debugpy";
+    const ADAPTER_NAME: &'static str = "Debugpy";
+    const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
-
-    pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
-        Ok(PythonDebugAdapter {
-            port: TcpTransport::port(host).await?,
-            host: host.host(),
-            timeout: host.timeout,
-        })
-    }
 }
 
 #[async_trait(?Send)]
@@ -34,7 +26,7 @@ impl DebugAdapter for PythonDebugAdapter {
         delegate: &dyn DapDelegate,
     ) -> Result<AdapterVersion> {
         let github_repo = GithubRepo {
-            repo_name: Self::ADAPTER_NAME.into(),
+            repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
             repo_owner: "microsoft".into(),
         };
 
@@ -78,12 +70,16 @@ impl DebugAdapter for PythonDebugAdapter {
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
+        let Some(tcp_connection) = config.tcp_connection.clone() else {
+            bail!("Python Debug Adapter expects tcp connection arguments to be provided");
+        };
+        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
         let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
             user_installed_path
         } else {
-            let adapter_path = paths::debug_adapters_dir().join(self.name());
-            let file_name_prefix = format!("{}_", self.name());
+            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
+            let file_name_prefix = format!("{}_", Self::ADAPTER_PACKAGE_NAME);
 
             util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
                 file_name.starts_with(&file_name_prefix)
@@ -118,25 +114,36 @@ impl DebugAdapter for PythonDebugAdapter {
             command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
             arguments: Some(vec![
                 debugpy_dir.join(Self::ADAPTER_PATH).into(),
-                format!("--port={}", self.port).into(),
-                format!("--host={}", self.host).into(),
+                format!("--port={}", port).into(),
+                format!("--host={}", host).into(),
             ]),
             connection: Some(adapters::TcpArguments {
-                host: self.host,
-                port: self.port,
-                timeout: self.timeout,
+                host,
+                port,
+                timeout,
             }),
-            cwd: config.cwd.clone(),
+            cwd: None,
             envs: None,
         })
     }
 
-    fn request_args(&self, config: &DebugAdapterConfig) -> Value {
-        json!({
-            "program": config.program,
-            "subProcess": true,
-            "cwd": config.cwd,
-            "redirectOutput": true,
-        })
+    fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        match &config.request {
+            DebugRequestType::Launch(launch_config) => {
+                json!({
+                    "program": launch_config.program,
+                    "subProcess": true,
+                    "cwd": launch_config.cwd,
+                    "redirectOutput": true,
+                })
+            }
+            dap::DebugRequestType::Attach(attach_config) => {
+                json!({
+                    "subProcess": true,
+                    "redirectOutput": true,
+                    "processId": attach_config.process_id
+                })
+            }
+        }
     }
 }

crates/debugger_ui/Cargo.toml 🔗

@@ -8,6 +8,10 @@ license = "GPL-3.0-or-later"
 [lints]
 workspace = true
 
+[lib]
+path = "src/debugger_ui.rs"
+doctest = false
+
 [features]
 test-support = [
     "dap/test-support",

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -3,8 +3,8 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::Subscription;
 use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
 use picker::{Picker, PickerDelegate};
-use project::debugger::attach_processes;
 
+use std::cell::LazyCell;
 use std::sync::Arc;
 use sysinfo::System;
 use ui::{prelude::*, Context, Tooltip};
@@ -13,10 +13,10 @@ use util::debug_panic;
 use workspace::ModalView;
 
 #[derive(Debug, Clone)]
-struct Candidate {
-    pid: u32,
-    name: String,
-    command: Vec<String>,
+pub(super) struct Candidate {
+    pub(super) pid: u32,
+    pub(super) name: SharedString,
+    pub(super) command: Vec<String>,
 }
 
 pub(crate) struct AttachModalDelegate {
@@ -24,16 +24,20 @@ pub(crate) struct AttachModalDelegate {
     matches: Vec<StringMatch>,
     placeholder_text: Arc<str>,
     project: Entity<project::Project>,
-    debug_config: task::DebugAdapterConfig,
-    candidates: Option<Vec<Candidate>>,
+    debug_config: task::DebugTaskDefinition,
+    candidates: Arc<[Candidate]>,
 }
 
 impl AttachModalDelegate {
-    pub fn new(project: Entity<project::Project>, debug_config: task::DebugAdapterConfig) -> Self {
+    fn new(
+        project: Entity<project::Project>,
+        debug_config: task::DebugTaskDefinition,
+        candidates: Arc<[Candidate]>,
+    ) -> Self {
         Self {
             project,
             debug_config,
-            candidates: None,
+            candidates,
             selected_index: 0,
             matches: Vec::default(),
             placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
@@ -49,12 +53,56 @@ pub struct AttachModal {
 impl AttachModal {
     pub fn new(
         project: Entity<project::Project>,
-        debug_config: task::DebugAdapterConfig,
+        debug_config: task::DebugTaskDefinition,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let mut processes: Vec<_> = System::new_all()
+            .processes()
+            .values()
+            .map(|process| {
+                let name = process.name().to_string_lossy().into_owned();
+                Candidate {
+                    name: name.into(),
+                    pid: process.pid().as_u32(),
+                    command: process
+                        .cmd()
+                        .iter()
+                        .map(|s| s.to_string_lossy().to_string())
+                        .collect::<Vec<_>>(),
+                }
+            })
+            .collect();
+        processes.sort_by_key(|k| k.name.clone());
+        Self::with_processes(project, debug_config, processes, window, cx)
+    }
+
+    pub(super) fn with_processes(
+        project: Entity<project::Project>,
+        debug_config: task::DebugTaskDefinition,
+        processes: Vec<Candidate>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
+        let adapter = project
+            .read(cx)
+            .debug_adapters()
+            .adapter(&debug_config.adapter);
+        let filter = LazyCell::new(|| adapter.map(|adapter| adapter.attach_processes_filter()));
+        let processes = processes
+            .into_iter()
+            .filter(|process| {
+                filter
+                    .as_ref()
+                    .map_or(false, |filter| filter.is_match(&process.name))
+            })
+            .collect();
         let picker = cx.new(|cx| {
-            Picker::uniform_list(AttachModalDelegate::new(project, debug_config), window, cx)
+            Picker::uniform_list(
+                AttachModalDelegate::new(project, debug_config, processes),
+                window,
+                cx,
+            )
         });
         Self {
             _subscription: cx.subscribe(&picker, |_, _, _, cx| {
@@ -116,32 +164,7 @@ impl PickerDelegate for AttachModalDelegate {
     ) -> gpui::Task<()> {
         cx.spawn(async move |this, cx| {
             let Some(processes) = this
-                .update(cx, |this, _| {
-                    if let Some(processes) = this.delegate.candidates.clone() {
-                        processes
-                    } else {
-                        let system = System::new_all();
-
-                        let processes =
-                            attach_processes(&this.delegate.debug_config.kind, &system.processes());
-                        let candidates = processes
-                            .into_iter()
-                            .map(|(pid, process)| Candidate {
-                                pid: pid.as_u32(),
-                                name: process.name().to_string_lossy().into_owned(),
-                                command: process
-                                    .cmd()
-                                    .iter()
-                                    .map(|s| s.to_string_lossy().to_string())
-                                    .collect::<Vec<_>>(),
-                            })
-                            .collect::<Vec<Candidate>>();
-
-                        let _ = this.delegate.candidates.insert(candidates.clone());
-
-                        candidates
-                    }
-                })
+                .update(cx, |this, _| this.delegate.candidates.clone())
                 .ok()
             else {
                 return;
@@ -176,7 +199,6 @@ impl PickerDelegate for AttachModalDelegate {
                 let delegate = &mut this.delegate;
 
                 delegate.matches = matches;
-                delegate.candidates = Some(processes);
 
                 if delegate.matches.is_empty() {
                     delegate.selected_index = 0;
@@ -195,7 +217,7 @@ impl PickerDelegate for AttachModalDelegate {
             .get(self.selected_index())
             .and_then(|current_match| {
                 let ix = current_match.candidate_id;
-                self.candidates.as_ref().map(|candidates| &candidates[ix])
+                self.candidates.get(ix)
             });
 
         let Some(candidate) = candidate else {
@@ -206,7 +228,7 @@ impl PickerDelegate for AttachModalDelegate {
             DebugRequestType::Attach(config) => {
                 config.process_id = Some(candidate.pid);
             }
-            DebugRequestType::Launch => {
+            DebugRequestType::Launch(_) => {
                 debug_panic!("Debugger attach modal used on launch debug config");
                 return;
             }
@@ -214,7 +236,13 @@ impl PickerDelegate for AttachModalDelegate {
 
         let config = self.debug_config.clone();
         self.project
-            .update(cx, |project, cx| project.start_debug_session(config, cx))
+            .update(cx, |project, cx| {
+                #[cfg(any(test, feature = "test-support"))]
+                let ret = project.fake_debug_session(config.request, None, false, cx);
+                #[cfg(not(any(test, feature = "test-support")))]
+                let ret = project.start_debug_session(config.into(), cx);
+                ret
+            })
             .detach_and_log_err(cx);
 
         cx.emit(DismissEvent);
@@ -222,7 +250,6 @@ impl PickerDelegate for AttachModalDelegate {
 
     fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
         self.selected_index = 0;
-        self.candidates.take();
 
         cx.emit(DismissEvent);
     }
@@ -234,9 +261,8 @@ impl PickerDelegate for AttachModalDelegate {
         _window: &mut Window,
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let candidates = self.candidates.as_ref()?;
         let hit = &self.matches[ix];
-        let candidate = &candidates.get(hit.candidate_id)?;
+        let candidate = self.candidates.get(hit.candidate_id)?;
 
         Some(
             ListItem::new(SharedString::from(format!("process-entry-{ix}")))
@@ -279,9 +305,8 @@ impl PickerDelegate for AttachModalDelegate {
     }
 }
 
-#[allow(dead_code)]
 #[cfg(any(test, feature = "test-support"))]
-pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
+pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
     modal.picker.update(cx, |picker, _| {
         picker
             .delegate

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -3,8 +3,8 @@ use anyhow::{anyhow, Result};
 use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
 use dap::{
-    client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, DebugAdapterConfig,
-    LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
+    client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
+    ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
 };
 use futures::{channel::mpsc, SinkExt as _};
 use gpui::{
@@ -19,6 +19,7 @@ use project::{
 use rpc::proto::{self};
 use settings::Settings;
 use std::{any::TypeId, path::PathBuf};
+use task::DebugTaskDefinition;
 use terminal_view::terminal_panel::TerminalPanel;
 use ui::prelude::*;
 use util::ResultExt;
@@ -52,7 +53,7 @@ pub struct DebugPanel {
     project: WeakEntity<Project>,
     workspace: WeakEntity<Workspace>,
     _subscriptions: Vec<Subscription>,
-    pub(crate) last_inert_config: Option<DebugAdapterConfig>,
+    pub(crate) last_inert_config: Option<DebugTaskDefinition>,
 }
 
 impl DebugPanel {

crates/debugger_ui/src/session.rs 🔗

@@ -6,7 +6,6 @@ mod starting;
 use std::time::Duration;
 
 use dap::client::SessionId;
-use dap::DebugAdapterConfig;
 use failed::FailedState;
 use gpui::{
     percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
@@ -19,6 +18,7 @@ use project::Project;
 use rpc::proto::{self, PeerId};
 use running::RunningState;
 use starting::{StartingEvent, StartingState};
+use task::DebugTaskDefinition;
 use ui::{prelude::*, Indicator};
 use util::ResultExt;
 use workspace::{
@@ -73,7 +73,7 @@ impl DebugSession {
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
         debug_panel: WeakEntity<DebugPanel>,
-        config: Option<DebugAdapterConfig>,
+        config: Option<DebugTaskDefinition>,
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
@@ -171,7 +171,7 @@ impl DebugSession {
             .flatten()
             .expect("worktree-less project");
         let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
-            store.new_session(config, &worktree, None, cx)
+            store.new_session(config.into(), &worktree, None, cx)
         }) else {
             return;
         };

crates/debugger_ui/src/session/inert.rs 🔗

@@ -1,10 +1,10 @@
 use std::path::PathBuf;
 
-use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
+use dap::DebugRequestType;
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
 use settings::Settings as _;
-use task::TCPHost;
+use task::{DebugTaskDefinition, LaunchConfig, TCPHost};
 use theme::ThemeSettings;
 use ui::{
     div, h_flex, relative, v_flex, ActiveTheme as _, ButtonCommon, ButtonLike, Clickable, Context,
@@ -35,7 +35,7 @@ impl SpawnMode {
 impl From<DebugRequestType> for SpawnMode {
     fn from(request: DebugRequestType) -> Self {
         match request {
-            DebugRequestType::Launch => SpawnMode::Launch,
+            DebugRequestType::Launch(_) => SpawnMode::Launch,
             DebugRequestType::Attach(_) => SpawnMode::Attach,
         }
     }
@@ -55,18 +55,13 @@ impl InertState {
     pub(super) fn new(
         workspace: WeakEntity<Workspace>,
         default_cwd: &str,
-        debug_config: Option<DebugAdapterConfig>,
+        debug_config: Option<DebugTaskDefinition>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let selected_debugger = debug_config.as_ref().and_then(|config| match config.kind {
-            DebugAdapterKind::Lldb => Some("LLDB".into()),
-            DebugAdapterKind::Go(_) => Some("Delve".into()),
-            DebugAdapterKind::Php(_) => Some("PHP".into()),
-            DebugAdapterKind::Javascript(_) => Some("JavaScript".into()),
-            DebugAdapterKind::Python(_) => Some("Debugpy".into()),
-            _ => None,
-        });
+        let selected_debugger = debug_config
+            .as_ref()
+            .map(|config| SharedString::from(config.adapter.clone()));
 
         let spawn_mode = debug_config
             .as_ref()
@@ -75,7 +70,10 @@ impl InertState {
 
         let program = debug_config
             .as_ref()
-            .and_then(|config| config.program.to_owned());
+            .and_then(|config| match &config.request {
+                DebugRequestType::Attach(_) => None,
+                DebugRequestType::Launch(launch_config) => Some(launch_config.program.clone()),
+            });
 
         let program_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
@@ -88,7 +86,10 @@ impl InertState {
         });
 
         let cwd = debug_config
-            .and_then(|config| config.cwd.map(|cwd| cwd.to_owned()))
+            .and_then(|config| match &config.request {
+                DebugRequestType::Attach(_) => None,
+                DebugRequestType::Launch(launch_config) => launch_config.cwd.clone(),
+            })
             .unwrap_or_else(|| PathBuf::from(default_cwd));
 
         let cwd_editor = cx.new(|cx| {
@@ -116,7 +117,7 @@ impl Focusable for InertState {
 }
 
 pub(crate) enum InertEvent {
-    Spawned { config: DebugAdapterConfig },
+    Spawned { config: DebugTaskDefinition },
 }
 
 impl EventEmitter<InertEvent> for InertState {}
@@ -130,6 +131,7 @@ impl Render for InertState {
         cx: &mut ui::Context<'_, Self>,
     ) -> impl ui::IntoElement {
         let weak = cx.weak_entity();
+        let workspace = self.workspace.clone();
         let disable_buttons = self.selected_debugger.is_none();
         let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
             .child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
@@ -137,21 +139,26 @@ impl Render for InertState {
                 if this.spawn_mode == SpawnMode::Launch {
                     let program = this.program_editor.read(cx).text(cx);
                     let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
-                    let kind =
-                        kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| {
+                    let kind = this
+                        .selected_debugger
+                        .as_deref()
+                        .unwrap_or_else(|| {
                             unimplemented!(
                                 "Automatic selection of a debugger based on users project"
                             )
-                        }));
+                        })
+                        .to_string();
+
                     cx.emit(InertEvent::Spawned {
-                        config: DebugAdapterConfig {
+                        config: DebugTaskDefinition {
                             label: "hard coded".into(),
-                            kind,
-                            request: DebugRequestType::Launch,
-                            program: Some(program),
-                            cwd: Some(cwd),
+                            adapter: kind,
+                            request: DebugRequestType::Launch(LaunchConfig {
+                                program,
+                                cwd: Some(cwd),
+                            }),
+                            tcp_connection: Some(TCPHost::default()),
                             initialize_args: None,
-                            supports_attach: false,
                         },
                     });
                 } else {
@@ -159,6 +166,7 @@ impl Render for InertState {
                 }
             }))
             .disabled(disable_buttons);
+
         v_flex()
             .track_focus(&self.focus_handle)
             .size_full()
@@ -179,28 +187,36 @@ impl Render for InertState {
                                         .as_ref()
                                         .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
                                         .clone(),
-                                    ContextMenu::build(window, cx, move |this, _, _| {
-                                        let setter_for_name = |name: &'static str| {
+                                    ContextMenu::build(window, cx, move |mut this, _, cx| {
+                                        let setter_for_name = |name: SharedString| {
                                             let weak = weak.clone();
                                             move |_: &mut Window, cx: &mut App| {
-                                                let name = name;
-                                                (&weak)
-                                                    .update(cx, move |this, _| {
-                                                        this.selected_debugger = Some(name.into());
-                                                    })
-                                                    .ok();
+                                                let name = name.clone();
+                                                weak.update(cx, move |this, cx| {
+                                                    this.selected_debugger = Some(name.clone());
+                                                    cx.notify();
+                                                })
+                                                .ok();
                                             }
                                         };
-                                        this.entry("GDB", None, setter_for_name("GDB"))
-                                            .entry("Delve", None, setter_for_name("Delve"))
-                                            .entry("LLDB", None, setter_for_name("LLDB"))
-                                            .entry("PHP", None, setter_for_name("PHP"))
-                                            .entry(
-                                                "JavaScript",
+                                        let available_adapters = workspace
+                                            .update(cx, |this, cx| {
+                                                this.project()
+                                                    .read(cx)
+                                                    .debug_adapters()
+                                                    .enumerate_adapters()
+                                            })
+                                            .ok()
+                                            .unwrap_or_default();
+
+                                        for adapter in available_adapters {
+                                            this = this.entry(
+                                                adapter.0.clone(),
                                                 None,
-                                                setter_for_name("JavaScript"),
-                                            )
-                                            .entry("Debugpy", None, setter_for_name("Debugpy"))
+                                                setter_for_name(adapter.0.clone()),
+                                            );
+                                        }
+                                        this
                                     }),
                                 )),
                             ),
@@ -265,18 +281,6 @@ impl Render for InertState {
     }
 }
 
-fn kind_for_label(label: &str) -> DebugAdapterKind {
-    match label {
-        "LLDB" => DebugAdapterKind::Lldb,
-        "Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
-        "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
-        "PHP" => DebugAdapterKind::Php(TCPHost::default()),
-        "Delve" => DebugAdapterKind::Go(TCPHost::default()),
-        _ => {
-            unimplemented!()
-        } // Maybe we should set a toast notification here
-    }
-}
 impl InertState {
     fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
@@ -302,19 +306,20 @@ impl InertState {
     }
 
     fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
-        let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
-        let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
-            unimplemented!("Automatic selection of a debugger based on users project")
-        }));
+        let kind = self
+            .selected_debugger
+            .as_deref()
+            .map(|s| s.to_string())
+            .unwrap_or_else(|| {
+                unimplemented!("Automatic selection of a debugger based on users project")
+            });
 
-        let config = DebugAdapterConfig {
+        let config = DebugTaskDefinition {
             label: "hard coded attach".into(),
-            kind,
+            adapter: kind,
             request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
-            program: None,
-            cwd: Some(cwd),
             initialize_args: None,
-            supports_attach: true,
+            tcp_connection: Some(TCPHost::default()),
         };
 
         let _ = self.workspace.update(cx, |workspace, cx| {

crates/debugger_ui/src/tests/attach_modal.rs 🔗

@@ -1,11 +1,11 @@
-use crate::*;
+use crate::{attach_modal::Candidate, *};
 use attach_modal::AttachModal;
-use dap::client::SessionId;
+use dap::{client::SessionId, FakeAdapter};
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use menu::Confirm;
 use project::{FakeFs, Project};
 use serde_json::json;
-use task::AttachConfig;
+use task::{AttachConfig, DebugTaskDefinition, TCPHost};
 use tests::{init_test, init_test_workspace};
 
 #[gpui::test]
@@ -27,14 +27,12 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(
-                dap::DebugRequestType::Attach(AttachConfig {
-                    process_id: Some(10),
-                }),
-                None,
-                None,
-            ),
+        project.fake_debug_session(
+            dap::DebugRequestType::Attach(AttachConfig {
+                process_id: Some(10),
+            }),
+            None,
+            false,
             cx,
         )
     });
@@ -83,13 +81,32 @@ async fn test_show_attach_modal_and_select_process(
     let attach_modal = workspace
         .update(cx, |workspace, window, cx| {
             workspace.toggle_modal(window, cx, |window, cx| {
-                AttachModal::new(
+                AttachModal::with_processes(
                     project.clone(),
-                    dap::test_config(
-                        dap::DebugRequestType::Attach(AttachConfig { process_id: None }),
-                        None,
-                        None,
-                    ),
+                    DebugTaskDefinition {
+                        adapter: FakeAdapter::ADAPTER_NAME.into(),
+                        request: dap::DebugRequestType::Attach(AttachConfig::default()),
+                        label: "attach example".into(),
+                        initialize_args: None,
+                        tcp_connection: Some(TCPHost::default()),
+                    },
+                    vec![
+                        Candidate {
+                            pid: 0,
+                            name: "fake-binary-1".into(),
+                            command: vec![],
+                        },
+                        Candidate {
+                            pid: 3,
+                            name: "non-fake-binary-1".into(),
+                            command: vec![],
+                        },
+                        Candidate {
+                            pid: 1,
+                            name: "fake-binary-2".into(),
+                            command: vec![],
+                        },
+                    ],
                     window,
                     cx,
                 )
@@ -105,10 +122,10 @@ async fn test_show_attach_modal_and_select_process(
     workspace
         .update(cx, |_, _, cx| {
             let names =
-                attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx));
+                attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
 
-            // we filtered out all processes that are not the current process(zed itself)
-            assert_eq!(1, names.len());
+            // we filtered out all processes that are not starting with `fake-binary`
+            assert_eq!(2, names.len());
         })
         .unwrap();
 

crates/debugger_ui/src/tests/console.rs 🔗

@@ -3,6 +3,7 @@ use dap::requests::StackTrace;
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use project::{FakeFs, Project};
 use serde_json::json;
+use task::LaunchConfig;
 use tests::{init_test, init_test_workspace};
 
 #[gpui::test]
@@ -29,8 +30,10 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
         .unwrap();
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });

crates/debugger_ui/src/tests/debugger_panel.rs 🔗

@@ -5,8 +5,8 @@ use dap::{
         Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
         StartDebugging, StepBack, StepIn, StepOut, Threads,
     },
-    DebugRequestType, ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint,
-    StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
+    ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, StartDebuggingRequestArguments,
+    StartDebuggingRequestArgumentsRequest,
 };
 use editor::{
     actions::{self},
@@ -25,6 +25,7 @@ use std::{
         Arc,
     },
 };
+use task::LaunchConfig;
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
 use tests::{active_debug_session_panel, init_test, init_test_workspace};
 use util::path;
@@ -49,7 +50,12 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -201,7 +207,12 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -385,7 +396,12 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -475,7 +491,12 @@ async fn test_handle_error_run_in_terminal_reverse_request(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -555,7 +576,12 @@ async fn test_handle_start_debugging_reverse_request(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -668,7 +694,12 @@ async fn test_shutdown_children_when_parent_session_shutdown(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let parent_session = task.await.unwrap();
@@ -776,7 +807,12 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let parent_session = task.await.unwrap();
@@ -891,15 +927,13 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(
-                DebugRequestType::Launch,
-                None,
-                Some(dap::Capabilities {
-                    supports_step_back: Some(true),
-                    ..Default::default()
-                }),
-            ),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            Some(dap::Capabilities {
+                supports_step_back: Some(true),
+                ..Default::default()
+            }),
+            false,
             cx,
         )
     });
@@ -1122,7 +1156,12 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
         .unwrap();
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -1347,7 +1386,12 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
     });
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
+            cx,
+        )
     });
 
     let session = task.await.unwrap();
@@ -1419,8 +1463,10 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(DebugRequestType::Launch, Some(true), None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            true,
             cx,
         )
     });

crates/debugger_ui/src/tests/module_list.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
 };
 use dap::{
     requests::{Modules, StackTrace, Threads},
-    DebugRequestType, StoppedEvent,
+    StoppedEvent,
 };
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use project::{FakeFs, Project};
@@ -13,6 +13,7 @@ use std::sync::{
     atomic::{AtomicBool, AtomicI32, Ordering},
     Arc,
 };
+use task::LaunchConfig;
 
 #[gpui::test]
 async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
@@ -30,15 +31,13 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(
-                DebugRequestType::Launch,
-                None,
-                Some(dap::Capabilities {
-                    supports_modules_request: Some(true),
-                    ..Default::default()
-                }),
-            ),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            Some(dap::Capabilities {
+                supports_modules_request: Some(true),
+                ..Default::default()
+            }),
+            false,
             cx,
         )
     });

crates/debugger_ui/src/tests/stack_frame_list.rs 🔗

@@ -12,6 +12,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use project::{FakeFs, Project};
 use serde_json::json;
 use std::sync::Arc;
+use task::LaunchConfig;
 use unindent::Unindent as _;
 use util::path;
 
@@ -52,8 +53,10 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -240,8 +243,10 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -513,8 +518,10 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });

crates/debugger_ui/src/tests/variable_list.rs 🔗

@@ -17,6 +17,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use menu::{SelectFirst, SelectNext, SelectPrevious};
 use project::{FakeFs, Project};
 use serde_json::json;
+use task::LaunchConfig;
 use unindent::Unindent as _;
 use util::path;
 
@@ -56,8 +57,10 @@ async fn test_basic_fetch_initial_scope_and_variables(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -283,8 +286,10 @@ async fn test_fetch_variables_for_multiple_scopes(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -562,8 +567,10 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -1362,8 +1369,10 @@ async fn test_variable_list_only_sends_requests_when_rendering(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });
@@ -1639,8 +1648,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     let task = project.update(cx, |project, cx| {
-        project.start_debug_session(
-            dap::test_config(dap::DebugRequestType::Launch, None, None),
+        project.fake_debug_session(
+            dap::DebugRequestType::Launch(LaunchConfig::default()),
+            None,
+            false,
             cx,
         )
     });

crates/evals/Cargo.toml 🔗

@@ -19,6 +19,7 @@ clap.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
+dap.workspace = true
 env_logger.workspace = true
 feature_flags.workspace = true
 fs.workspace = true

crates/evals/src/eval.rs 🔗

@@ -4,6 +4,7 @@ use clap::Parser;
 use client::{Client, UserStore};
 use clock::RealSystemClock;
 use collections::BTreeMap;
+use dap::DapRegistry;
 use feature_flags::FeatureFlagAppExt as _;
 use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
 use http_client::{HttpClient, Method};
@@ -302,6 +303,7 @@ async fn run_evaluation(
     ));
 
     let language_registry = Arc::new(LanguageRegistry::new(executor.clone()));
+    let debug_adapters = Arc::new(DapRegistry::default());
     cx.update(|cx| languages::init(language_registry.clone(), node_runtime.clone(), cx))
         .unwrap();
 
@@ -346,6 +348,7 @@ async fn run_evaluation(
                     node_runtime.clone(),
                     user_store.clone(),
                     language_registry.clone(),
+                    debug_adapters.clone(),
                     fs.clone(),
                     None,
                     cx,

crates/project/Cargo.toml 🔗

@@ -37,7 +37,6 @@ client.workspace = true
 clock.workspace = true
 collections.workspace = true
 dap.workspace = true
-dap_adapters.workspace = true
 extension.workspace = true
 fancy-regex.workspace = true
 fs.workspace = true

crates/project/src/debugger.rs 🔗

@@ -15,5 +15,3 @@ pub mod breakpoint_store;
 pub mod dap_command;
 pub mod dap_store;
 pub mod session;
-
-pub use dap_adapters::attach_processes;

crates/project/src/debugger/dap_store.rs 🔗

@@ -20,10 +20,9 @@ use dap::{
         Completions, Evaluate, Request as _, RunInTerminal, SetExpression, SetVariable,
         StartDebugging,
     },
-    Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, EvaluateArguments,
-    EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
+    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, ErrorResponse,
+    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
     SetExpressionArguments, SetVariableArguments, Source, StartDebuggingRequestArguments,
-    StartDebuggingRequestArgumentsRequest,
 };
 use fs::Fs;
 use futures::{
@@ -51,7 +50,7 @@ use std::{
     sync::{atomic::Ordering::SeqCst, Arc},
 };
 use std::{collections::VecDeque, sync::atomic::AtomicU32};
-use task::{AttachConfig, DebugAdapterConfig, DebugRequestType};
+use task::{DebugAdapterConfig, DebugRequestDisposition};
 use util::ResultExt as _;
 use worktree::Worktree;
 
@@ -89,6 +88,7 @@ pub struct LocalDapStore {
     worktree_store: Entity<WorktreeStore>,
     environment: Entity<ProjectEnvironment>,
     language_registry: Arc<LanguageRegistry>,
+    debug_adapters: Arc<DapRegistry>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
     start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
     _start_debugging_task: Task<()>,
@@ -138,6 +138,7 @@ impl DapStore {
         node_runtime: NodeRuntime,
         fs: Arc<dyn Fs>,
         language_registry: Arc<LanguageRegistry>,
+        debug_adapters: Arc<DapRegistry>,
         environment: Entity<ProjectEnvironment>,
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         breakpoint_store: Entity<BreakpointStore>,
@@ -178,6 +179,7 @@ impl DapStore {
                 worktree_store,
                 toolchain_store,
                 language_registry,
+                debug_adapters,
                 start_debugging_tx,
                 _start_debugging_task,
                 next_session_id: Default::default(),
@@ -364,52 +366,63 @@ impl DapStore {
             config,
             local_store.start_debugging_tx.clone(),
             initialized_tx,
+            local_store.debug_adapters.clone(),
             cx,
         );
 
-        let task = cx.spawn(async move |this, cx| {
-            let session = match start_client_task.await {
-                Ok(session) => session,
-                Err(error) => {
-                    this.update(cx, |_, cx| {
-                        cx.emit(DapStoreEvent::Notification(error.to_string()));
-                    })
-                    .log_err();
+        let task = create_new_session(session_id, initialized_rx, start_client_task, cx);
+        (session_id, task)
+    }
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn new_fake_session(
+        &mut self,
+        config: DebugAdapterConfig,
+        worktree: &Entity<Worktree>,
+        parent_session: Option<Entity<Session>>,
+        caps: Capabilities,
+        fails: bool,
+        cx: &mut Context<Self>,
+    ) -> (SessionId, Task<Result<Entity<Session>>>) {
+        let Some(local_store) = self.as_local() else {
+            unimplemented!("Starting session on remote side");
+        };
 
-                    return Err(error);
-                }
-            };
+        let delegate = DapAdapterDelegate::new(
+            local_store.fs.clone(),
+            worktree.read(cx).id(),
+            local_store.node_runtime.clone(),
+            local_store.http_client.clone(),
+            local_store.language_registry.clone(),
+            local_store.toolchain_store.clone(),
+            local_store.environment.update(cx, |env, cx| {
+                let worktree = worktree.read(cx);
+                env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
+            }),
+        );
+        let session_id = local_store.next_session_id();
 
-            // we have to insert the session early, so we can handle reverse requests
-            // that need the session to be available
-            this.update(cx, |store, cx| {
-                store.sessions.insert(session_id, session.clone());
-                cx.emit(DapStoreEvent::DebugClientStarted(session_id));
-                cx.notify();
-            })?;
+        if let Some(session) = &parent_session {
+            session.update(cx, |session, _| {
+                session.add_child_session_id(session_id);
+            });
+        }
 
-            match session
-                .update(cx, |session, cx| {
-                    session.initialize_sequence(initialized_rx, cx)
-                })?
-                .await
-            {
-                Ok(_) => {}
-                Err(error) => {
-                    this.update(cx, |this, cx| {
-                        cx.emit(DapStoreEvent::Notification(error.to_string()));
-
-                        this.shutdown_session(session_id, cx)
-                    })?
-                    .await
-                    .log_err();
-
-                    return Err(error);
-                }
-            }
+        let (initialized_tx, initialized_rx) = oneshot::channel();
 
-            Ok(session)
-        });
+        let start_client_task = Session::fake(
+            self.breakpoint_store.clone(),
+            session_id,
+            parent_session,
+            delegate,
+            config,
+            local_store.start_debugging_tx.clone(),
+            initialized_tx,
+            caps,
+            fails,
+            cx,
+        );
+
+        let task = create_new_session(session_id, initialized_rx, start_client_task, cx);
         (session_id, task)
     }
 
@@ -431,7 +444,6 @@ impl DapStore {
             request.arguments.unwrap_or_default(),
         )
         .expect("To parse StartDebuggingRequestArguments");
-
         let worktree = local_store
             .worktree_store
             .update(cx, |this, _| this.worktrees().next())
@@ -441,25 +453,30 @@ impl DapStore {
             unreachable!("there must be a config for local sessions");
         };
 
-        let (_, new_session_task) = self.new_session(
-            DebugAdapterConfig {
-                label: config.label,
-                kind: config.kind,
-                request: match &args.request {
-                    StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch,
-                    StartDebuggingRequestArgumentsRequest::Attach => {
-                        DebugRequestType::Attach(AttachConfig::default())
-                    }
-                },
-                program: config.program,
-                cwd: config.cwd,
-                initialize_args: Some(args.configuration),
-                supports_attach: config.supports_attach,
-            },
-            &worktree,
-            Some(parent_session.clone()),
-            cx,
-        );
+        let debug_config = DebugAdapterConfig {
+            label: config.label,
+            adapter: config.adapter,
+            request: DebugRequestDisposition::ReverseRequest(args),
+            initialize_args: config.initialize_args.clone(),
+            tcp_connection: config.tcp_connection.clone(),
+        };
+        #[cfg(any(test, feature = "test-support"))]
+        let new_session_task = {
+            let caps = parent_session.read(cx).capabilities.clone();
+            self.new_fake_session(
+                debug_config,
+                &worktree,
+                Some(parent_session.clone()),
+                caps,
+                false,
+                cx,
+            )
+            .1
+        };
+        #[cfg(not(any(test, feature = "test-support")))]
+        let new_session_task = self
+            .new_session(debug_config, &worktree, Some(parent_session.clone()), cx)
+            .1;
 
         let request_seq = request.seq;
         cx.spawn(async move |_, cx| {
@@ -830,6 +847,58 @@ impl DapStore {
     }
 }
 
+fn create_new_session(
+    session_id: SessionId,
+    initialized_rx: oneshot::Receiver<()>,
+    start_client_task: Task<Result<Entity<Session>, anyhow::Error>>,
+    cx: &mut Context<'_, DapStore>,
+) -> Task<Result<Entity<Session>>> {
+    let task = cx.spawn(async move |this, cx| {
+        let session = match start_client_task.await {
+            Ok(session) => session,
+            Err(error) => {
+                this.update(cx, |_, cx| {
+                    cx.emit(DapStoreEvent::Notification(error.to_string()));
+                })
+                .log_err();
+
+                return Err(error);
+            }
+        };
+
+        // we have to insert the session early, so we can handle reverse requests
+        // that need the session to be available
+        this.update(cx, |store, cx| {
+            store.sessions.insert(session_id, session.clone());
+            cx.emit(DapStoreEvent::DebugClientStarted(session_id));
+            cx.notify();
+        })?;
+
+        match session
+            .update(cx, |session, cx| {
+                session.initialize_sequence(initialized_rx, cx)
+            })?
+            .await
+        {
+            Ok(_) => {}
+            Err(error) => {
+                this.update(cx, |this, cx| {
+                    cx.emit(DapStoreEvent::Notification(error.to_string()));
+
+                    this.shutdown_session(session_id, cx)
+                })?
+                .await
+                .log_err();
+
+                return Err(error);
+            }
+        }
+
+        Ok(session)
+    });
+    task
+}
+
 #[derive(Clone)]
 pub struct DapAdapterDelegate {
     fs: Arc<dyn Fs>,

crates/project/src/debugger/session.rs 🔗

@@ -14,7 +14,6 @@ use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, IndexMap, IndexSet};
 use dap::adapters::{DebugAdapter, DebugAdapterBinary};
 use dap::messages::Response;
-use dap::OutputEventCategory;
 use dap::{
     adapters::{DapDelegate, DapStatus},
     client::{DebugAdapterClient, SessionId},
@@ -22,7 +21,7 @@ use dap::{
     Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
     SteppingGranularity, StoppedEvent, VariableReference,
 };
-use dap_adapters::build_adapter;
+use dap::{DapRegistry, DebugRequestType, OutputEventCategory};
 use futures::channel::oneshot;
 use futures::{future::Shared, FutureExt};
 use gpui::{
@@ -42,7 +41,7 @@ use std::{
     path::Path,
     sync::Arc,
 };
-use task::DebugAdapterConfig;
+use task::{DebugAdapterConfig, DebugTaskDefinition};
 use text::{PointUtf16, ToPointUtf16};
 use util::{merge_json_value_into, ResultExt};
 
@@ -183,6 +182,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
 
 impl LocalMode {
     fn new(
+        debug_adapters: Arc<DapRegistry>,
         session_id: SessionId,
         parent_session: Option<Entity<Session>>,
         breakpoint_store: Entity<BreakpointStore>,
@@ -191,55 +191,64 @@ impl LocalMode {
         messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
         cx: AsyncApp,
     ) -> Task<Result<(Self, Capabilities)>> {
-        cx.spawn(async move |cx| {
-            let (adapter, binary) = Self::get_adapter_binary(&config, &delegate,  cx).await?;
-
-            let message_handler = Box::new(move |message| {
-                messages_tx.unbounded_send(message).ok();
-            });
-
-            let client = Arc::new(
-                if let Some(client) = parent_session
-                    .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
-                    .flatten()
-                {
-                    client
-                        .reconnect(session_id, binary, message_handler, cx.clone())
-                        .await?
-                } else {
-                    DebugAdapterClient::start(
-                        session_id,
-                        adapter.name(),
-                        binary,
-                        message_handler,
-                        cx.clone(),
-                    )
-                    .await?
-                },
-            );
-
-            let adapter_id = adapter.name().to_string().to_owned();
-            let session = Self {
-                client,
-                adapter,
-                breakpoint_store,
-                config: config.clone(),
-            };
-
-            #[cfg(any(test, feature = "test-support"))]
-            {
-                let dap::DebugAdapterKind::Fake((fail, caps)) = session.config.kind.clone() else {
-                    panic!("Only fake debug adapter configs should be used in tests");
-                };
+        Self::new_inner(
+            debug_adapters,
+            session_id,
+            parent_session,
+            breakpoint_store,
+            config,
+            delegate,
+            messages_tx,
+            async |_, _| {},
+            cx,
+        )
+    }
+    #[cfg(any(test, feature = "test-support"))]
+    fn new_fake(
+        session_id: SessionId,
+        parent_session: Option<Entity<Session>>,
+        breakpoint_store: Entity<BreakpointStore>,
+        config: DebugAdapterConfig,
+        delegate: DapAdapterDelegate,
+        messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
+        caps: Capabilities,
+        fail: bool,
+        cx: AsyncApp,
+    ) -> Task<Result<(Self, Capabilities)>> {
+        use task::DebugRequestDisposition;
+
+        let request = match config.request.clone() {
+            DebugRequestDisposition::UserConfigured(request) => request,
+            DebugRequestDisposition::ReverseRequest(reverse_request_args) => {
+                match reverse_request_args.request {
+                    dap::StartDebuggingRequestArgumentsRequest::Launch => {
+                        DebugRequestType::Launch(task::LaunchConfig {
+                            program: "".to_owned(),
+                            cwd: None,
+                        })
+                    }
+                    dap::StartDebuggingRequestArgumentsRequest::Attach => {
+                        DebugRequestType::Attach(task::AttachConfig {
+                            process_id: Some(0),
+                        })
+                    }
+                }
+            }
+        };
 
-                session
-                    .client
-                    .on_request::<dap::requests::Initialize, _>(move |_, _| Ok(caps.clone()))
-                    .await;
+        let callback = async move |session: &mut LocalMode, cx: AsyncApp| {
+            session
+                .client
+                .on_request::<dap::requests::Initialize, _>(move |_, _| Ok(caps.clone()))
+                .await;
 
-                let paths = cx.update(|cx| session.breakpoint_store.read(cx).breakpoint_paths()).expect("Breakpoint store should exist in all tests that start debuggers");
+            let paths = cx
+                .update(|cx| session.breakpoint_store.read(cx).breakpoint_paths())
+                .expect("Breakpoint store should exist in all tests that start debuggers");
 
-                session.client.on_request::<dap::requests::SetBreakpoints, _>(move |_, args| {
+            session
+                .client
+                .on_request::<dap::requests::SetBreakpoints, _>(move |_, args| {
                     let p = Arc::from(Path::new(&args.source.path.unwrap()));
                     if !paths.contains(&p) {
                         panic!("Sent breakpoints for path without any")
@@ -248,10 +257,12 @@ impl LocalMode {
                     Ok(dap::SetBreakpointsResponse {
                         breakpoints: Vec::default(),
                     })
-                }).await;
+                })
+                .await;
 
-                match config.request.clone() {
-                    dap::DebugRequestType::Launch if fail => {
+            match request {
+                dap::DebugRequestType::Launch(_) => {
+                    if fail {
                         session
                             .client
                             .on_request::<dap::requests::Launch, _>(move |_, _| {
@@ -268,14 +279,15 @@ impl LocalMode {
                                 })
                             })
                             .await;
-                    }
-                    dap::DebugRequestType::Launch => {
+                    } else {
                         session
                             .client
                             .on_request::<dap::requests::Launch, _>(move |_, _| Ok(()))
                             .await;
                     }
-                    dap::DebugRequestType::Attach(_) if fail => {
+                }
+                dap::DebugRequestType::Attach(attach_config) => {
+                    if fail {
                         session
                             .client
                             .on_request::<dap::requests::Attach, _>(move |_, _| {
@@ -292,8 +304,7 @@ impl LocalMode {
                                 })
                             })
                             .await;
-                    }
-                    dap::DebugRequestType::Attach(attach_config) => {
+                    } else {
                         session
                             .client
                             .on_request::<dap::requests::Attach, _>(move |_, args| {
@@ -307,11 +318,74 @@ impl LocalMode {
                             .await;
                     }
                 }
-
-                session.client.on_request::<dap::requests::Disconnect, _>(move |_, _| Ok(())).await;
-                session.client.fake_event(Events::Initialized(None)).await;
             }
 
+            session
+                .client
+                .on_request::<dap::requests::Disconnect, _>(move |_, _| Ok(()))
+                .await;
+            session.client.fake_event(Events::Initialized(None)).await;
+        };
+        Self::new_inner(
+            DapRegistry::fake().into(),
+            session_id,
+            parent_session,
+            breakpoint_store,
+            config,
+            delegate,
+            messages_tx,
+            callback,
+            cx,
+        )
+    }
+    fn new_inner(
+        registry: Arc<DapRegistry>,
+        session_id: SessionId,
+        parent_session: Option<Entity<Session>>,
+        breakpoint_store: Entity<BreakpointStore>,
+        config: DebugAdapterConfig,
+        delegate: DapAdapterDelegate,
+        messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
+        on_initialized: impl AsyncFnOnce(&mut LocalMode, AsyncApp) + 'static,
+        cx: AsyncApp,
+    ) -> Task<Result<(Self, Capabilities)>> {
+        cx.spawn(async move |cx| {
+            let (adapter, binary) =
+                Self::get_adapter_binary(&registry, &config, &delegate, cx).await?;
+
+            let message_handler = Box::new(move |message| {
+                messages_tx.unbounded_send(message).ok();
+            });
+
+            let client = Arc::new(
+                if let Some(client) = parent_session
+                    .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
+                    .flatten()
+                {
+                    client
+                        .reconnect(session_id, binary, message_handler, cx.clone())
+                        .await?
+                } else {
+                    DebugAdapterClient::start(
+                        session_id,
+                        adapter.name(),
+                        binary,
+                        message_handler,
+                        cx.clone(),
+                    )
+                    .await?
+                },
+            );
+
+            let adapter_id = adapter.name().to_string().to_owned();
+            let mut session = Self {
+                client,
+                adapter,
+                breakpoint_store,
+                config: config.clone(),
+            };
+
+            on_initialized(&mut session, cx.clone()).await;
             let capabilities = session
                 .request(Initialize { adapter_id }, cx.background_executor().clone())
                 .await?;
@@ -420,11 +494,14 @@ impl LocalMode {
     }
 
     async fn get_adapter_binary(
+        registry: &Arc<DapRegistry>,
         config: &DebugAdapterConfig,
         delegate: &DapAdapterDelegate,
         cx: &mut AsyncApp,
     ) -> Result<(Arc<dyn DebugAdapter>, DebugAdapterBinary)> {
-        let adapter = build_adapter(&config.kind).await?;
+        let adapter = registry
+            .adapter(&config.adapter)
+            .ok_or_else(|| anyhow!("Debug adapter with name `{}` was not found", config.adapter))?;
 
         let binary = cx.update(|cx| {
             ProjectSettings::get_global(cx)
@@ -465,20 +542,36 @@ impl LocalMode {
         initialized_rx: oneshot::Receiver<()>,
         cx: &App,
     ) -> Task<Result<()>> {
-        let mut raw = self.adapter.request_args(&self.config);
+        let (mut raw, is_launch) = match &self.config.request {
+            task::DebugRequestDisposition::UserConfigured(_) => {
+                let Ok(raw) = DebugTaskDefinition::try_from(self.config.clone()) else {
+                    debug_assert!(false, "This part of code should be unreachable in practice");
+                    return Task::ready(Err(anyhow!(
+                        "Expected debug config conversion to succeed"
+                    )));
+                };
+                let is_launch = matches!(raw.request, DebugRequestType::Launch(_));
+                let raw = self.adapter.request_args(&raw);
+                (raw, is_launch)
+            }
+            task::DebugRequestDisposition::ReverseRequest(start_debugging_request_arguments) => (
+                start_debugging_request_arguments.configuration.clone(),
+                matches!(
+                    start_debugging_request_arguments.request,
+                    dap::StartDebuggingRequestArgumentsRequest::Launch
+                ),
+            ),
+        };
+
         merge_json_value_into(
             self.config.initialize_args.clone().unwrap_or(json!({})),
             &mut raw,
         );
-
         // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
-        let launch = match &self.config.request {
-            dap::DebugRequestType::Launch => {
-                self.request(Launch { raw }, cx.background_executor().clone())
-            }
-            dap::DebugRequestType::Attach(_) => {
-                self.request(Attach { raw }, cx.background_executor().clone())
-            }
+        let launch = if is_launch {
+            self.request(Launch { raw }, cx.background_executor().clone())
+        } else {
+            self.request(Attach { raw }, cx.background_executor().clone())
         };
 
         let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
@@ -745,12 +838,14 @@ impl Session {
         config: DebugAdapterConfig,
         start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
         initialized_tx: oneshot::Sender<()>,
+        debug_adapters: Arc<DapRegistry>,
         cx: &mut App,
     ) -> Task<Result<Entity<Self>>> {
-        let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded();
+        let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
 
         cx.spawn(async move |cx| {
             let (mode, capabilities) = LocalMode::new(
+                debug_adapters,
                 session_id,
                 parent_session.clone(),
                 breakpoint_store.clone(),
@@ -762,74 +857,62 @@ impl Session {
             .await?;
 
             cx.new(|cx| {
-                let _background_tasks = vec![cx.spawn(async move |this: WeakEntity<Self>, cx| {
-                    let mut initialized_tx = Some(initialized_tx);
-                    while let Some(message) = message_rx.next().await {
-                        if let Message::Event(event) = message {
-                            if let Events::Initialized(_) = *event {
-                                if let Some(tx) = initialized_tx.take() {
-                                    tx.send(()).ok();
-                                }
-                            } else {
-                                let Ok(_) = this.update(cx, |session, cx| {
-                                    session.handle_dap_event(event, cx);
-                                }) else {
-                                    break;
-                                };
-                            }
-                        } else {
-                            let Ok(_) =
-                                start_debugging_requests_tx.unbounded_send((session_id, message))
-                            else {
-                                break;
-                            };
-                        }
-                    }
-                })];
+                create_local_session(
+                    breakpoint_store,
+                    session_id,
+                    parent_session,
+                    start_debugging_requests_tx,
+                    initialized_tx,
+                    message_rx,
+                    mode,
+                    capabilities,
+                    cx,
+                )
+            })
+        })
+    }
 
-                cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
-                    BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
-                        if let Some(local) = (!this.ignore_breakpoints)
-                            .then(|| this.as_local_mut())
-                            .flatten()
-                        {
-                            local
-                                .send_breakpoints_from_path(path.clone(), *reason, cx)
-                                .detach();
-                        };
-                    }
-                    BreakpointStoreEvent::BreakpointsCleared(paths) => {
-                        if let Some(local) = (!this.ignore_breakpoints)
-                            .then(|| this.as_local_mut())
-                            .flatten()
-                        {
-                            local.unset_breakpoints_from_paths(paths, cx).detach();
-                        }
-                    }
-                    BreakpointStoreEvent::ActiveDebugLineChanged => {}
-                })
-                .detach();
-
-                Self {
-                    mode: Mode::Local(mode),
-                    id: session_id,
-                    child_session_ids: HashSet::default(),
-                    parent_id: parent_session.map(|session| session.read(cx).id),
-                    variables: Default::default(),
+    #[cfg(any(test, feature = "test-support"))]
+    pub(crate) fn fake(
+        breakpoint_store: Entity<BreakpointStore>,
+        session_id: SessionId,
+        parent_session: Option<Entity<Session>>,
+        delegate: DapAdapterDelegate,
+        config: DebugAdapterConfig,
+        start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
+        initialized_tx: oneshot::Sender<()>,
+        caps: Capabilities,
+        fails: bool,
+        cx: &mut App,
+    ) -> Task<Result<Entity<Session>>> {
+        let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
+
+        cx.spawn(async move |cx| {
+            let (mode, capabilities) = LocalMode::new_fake(
+                session_id,
+                parent_session.clone(),
+                breakpoint_store.clone(),
+                config.clone(),
+                delegate,
+                message_tx,
+                caps,
+                fails,
+                cx.clone(),
+            )
+            .await?;
+
+            cx.new(|cx| {
+                create_local_session(
+                    breakpoint_store,
+                    session_id,
+                    parent_session,
+                    start_debugging_requests_tx,
+                    initialized_tx,
+                    message_rx,
+                    mode,
                     capabilities,
-                    thread_states: ThreadStates::default(),
-                    output_token: OutputToken(0),
-                    ignore_breakpoints: false,
-                    output: circular_buffer::CircularBuffer::boxed(),
-                    requests: HashMap::default(),
-                    modules: Vec::default(),
-                    loaded_sources: Vec::default(),
-                    threads: IndexMap::default(),
-                    stack_frames: IndexMap::default(),
-                    locations: Default::default(),
-                    _background_tasks,
-                    is_session_terminated: false,
-                }
+                    cx,
+                )
             })
         })
     }
@@ -1838,3 +1921,83 @@ impl Session {
         }
     }
 }
+
+fn create_local_session(
+    breakpoint_store: Entity<BreakpointStore>,
+    session_id: SessionId,
+    parent_session: Option<Entity<Session>>,
+    start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
+    initialized_tx: oneshot::Sender<()>,
+    mut message_rx: futures::channel::mpsc::UnboundedReceiver<Message>,
+    mode: LocalMode,
+    capabilities: Capabilities,
+    cx: &mut Context<'_, Session>,
+) -> Session {
+    let _background_tasks = vec![cx.spawn(async move |this: WeakEntity<Session>, cx| {
+        let mut initialized_tx = Some(initialized_tx);
+        while let Some(message) = message_rx.next().await {
+            if let Message::Event(event) = message {
+                if let Events::Initialized(_) = *event {
+                    if let Some(tx) = initialized_tx.take() {
+                        tx.send(()).ok();
+                    }
+                } else {
+                    let Ok(_) = this.update(cx, |session, cx| {
+                        session.handle_dap_event(event, cx);
+                    }) else {
+                        break;
+                    };
+                }
+            } else {
+                let Ok(_) = start_debugging_requests_tx.unbounded_send((session_id, message))
+                else {
+                    break;
+                };
+            }
+        }
+    })];
+
+    cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
+        BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
+            if let Some(local) = (!this.ignore_breakpoints)
+                .then(|| this.as_local_mut())
+                .flatten()
+            {
+                local
+                    .send_breakpoints_from_path(path.clone(), *reason, cx)
+                    .detach();
+            };
+        }
+        BreakpointStoreEvent::BreakpointsCleared(paths) => {
+            if let Some(local) = (!this.ignore_breakpoints)
+                .then(|| this.as_local_mut())
+                .flatten()
+            {
+                local.unset_breakpoints_from_paths(paths, cx).detach();
+            }
+        }
+        BreakpointStoreEvent::ActiveDebugLineChanged => {}
+    })
+    .detach();
+
+    Session {
+        mode: Mode::Local(mode),
+        id: session_id,
+        child_session_ids: HashSet::default(),
+        parent_id: parent_session.map(|session| session.read(cx).id),
+        variables: Default::default(),
+        capabilities,
+        thread_states: ThreadStates::default(),
+        output_token: OutputToken(0),
+        ignore_breakpoints: false,
+        output: circular_buffer::CircularBuffer::boxed(),
+        requests: HashMap::default(),
+        modules: Vec::default(),
+        loaded_sources: Vec::default(),
+        threads: IndexMap::default(),
+        stack_frames: IndexMap::default(),
+        locations: Default::default(),
+        _background_tasks,
+        is_session_terminated: false,
+    }
+}

crates/project/src/project.rs 🔗

@@ -38,7 +38,7 @@ use client::{
 };
 use clock::ReplicaId;
 
-use dap::{client::DebugAdapterClient, DebugAdapterConfig};
+use dap::{client::DebugAdapterClient, DapRegistry, DebugAdapterConfig};
 
 use collections::{BTreeSet, HashMap, HashSet};
 use debounced_delay::DebouncedDelay;
@@ -163,6 +163,7 @@ pub struct Project {
     active_entry: Option<ProjectEntryId>,
     buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
     languages: Arc<LanguageRegistry>,
+    debug_adapters: Arc<DapRegistry>,
     dap_store: Entity<DapStore>,
     breakpoint_store: Entity<BreakpointStore>,
     client: Arc<client::Client>,
@@ -818,6 +819,7 @@ impl Project {
         node: NodeRuntime,
         user_store: Entity<UserStore>,
         languages: Arc<LanguageRegistry>,
+        debug_adapters: Arc<DapRegistry>,
         fs: Arc<dyn Fs>,
         env: Option<HashMap<String, String>>,
         cx: &mut App,
@@ -854,6 +856,7 @@ impl Project {
                     node.clone(),
                     fs.clone(),
                     languages.clone(),
+                    debug_adapters.clone(),
                     environment.clone(),
                     toolchain_store.read(cx).as_language_toolchain_store(),
                     breakpoint_store.clone(),
@@ -940,6 +943,7 @@ impl Project {
                 active_entry: None,
                 snippets,
                 languages,
+                debug_adapters,
                 client,
                 task_store,
                 user_store,
@@ -1102,6 +1106,7 @@ impl Project {
                 active_entry: None,
                 snippets,
                 languages,
+                debug_adapters: Arc::new(DapRegistry::default()),
                 client,
                 task_store,
                 user_store,
@@ -1239,7 +1244,6 @@ impl Project {
 
         let breakpoint_store =
             cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
-
         let dap_store = cx.new(|_cx| {
             DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone())
         })?;
@@ -1326,6 +1330,7 @@ impl Project {
                 collaborators: Default::default(),
                 join_project_response_message_id: response.message_id,
                 languages,
+                debug_adapters: Arc::new(DapRegistry::default()),
                 user_store: user_store.clone(),
                 task_store,
                 snippets,
@@ -1450,13 +1455,7 @@ impl Project {
         config: DebugAdapterConfig,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Session>>> {
-        let worktree = maybe!({
-            if let Some(cwd) = &config.cwd {
-                Some(self.find_worktree(cwd.as_path(), cx)?.0)
-            } else {
-                self.worktrees(cx).next()
-            }
-        });
+        let worktree = maybe!({ self.worktrees(cx).next() });
 
         let Some(worktree) = &worktree else {
             return Task::ready(Err(anyhow!("Failed to find a worktree")));
@@ -1469,6 +1468,40 @@ impl Project {
             .1
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn fake_debug_session(
+        &mut self,
+        request: task::DebugRequestType,
+        caps: Option<dap::Capabilities>,
+        fails: bool,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Entity<Session>>> {
+        use dap::{Capabilities, FakeAdapter};
+        use task::DebugRequestDisposition;
+
+        let worktree = maybe!({ self.worktrees(cx).next() });
+
+        let Some(worktree) = &worktree else {
+            return Task::ready(Err(anyhow!("Failed to find a worktree")));
+        };
+        let config = DebugAdapterConfig {
+            label: "test config".into(),
+            adapter: FakeAdapter::ADAPTER_NAME.into(),
+            request: DebugRequestDisposition::UserConfigured(request),
+            initialize_args: None,
+            tcp_connection: None,
+        };
+        let caps = caps.unwrap_or(Capabilities {
+            supports_step_back: Some(false),
+            ..Default::default()
+        });
+        self.dap_store
+            .update(cx, |dap_store, cx| {
+                dap_store.new_fake_session(config, worktree, None, caps, fails, cx)
+            })
+            .1
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub async fn example(
         root_paths: impl IntoIterator<Item = &Path>,
@@ -1478,6 +1511,7 @@ impl Project {
 
         let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
         let languages = LanguageRegistry::test(cx.background_executor().clone());
+        let debug_adapters = DapRegistry::default().into();
         let clock = Arc::new(FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();
         let client = cx
@@ -1491,6 +1525,7 @@ impl Project {
                     node_runtime::NodeRuntime::unavailable(),
                     user_store,
                     Arc::new(languages),
+                    debug_adapters,
                     fs,
                     None,
                     cx,
@@ -1521,6 +1556,7 @@ impl Project {
         use clock::FakeSystemClock;
 
         let languages = LanguageRegistry::test(cx.executor());
+        let debug_adapters = DapRegistry::fake();
         let clock = Arc::new(FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
@@ -1531,6 +1567,7 @@ impl Project {
                 node_runtime::NodeRuntime::unavailable(),
                 user_store,
                 Arc::new(languages),
+                Arc::new(debug_adapters),
                 fs,
                 None,
                 cx,
@@ -1574,6 +1611,10 @@ impl Project {
         &self.languages
     }
 
+    pub fn debug_adapters(&self) -> &Arc<DapRegistry> {
+        &self.debug_adapters
+    }
+
     pub fn client(&self) -> Arc<Client> {
         self.client.clone()
     }

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -557,6 +557,7 @@ pub async fn open_ssh_project(
                 app_state.node_runtime.clone(),
                 app_state.user_store.clone(),
                 app_state.languages.clone(),
+                app_state.debug_adapters.clone(),
                 app_state.fs.clone(),
                 None,
                 cx,

crates/remote_server/Cargo.toml 🔗

@@ -28,6 +28,7 @@ backtrace = "0.3"
 chrono.workspace = true
 clap.workspace = true
 client.workspace = true
+dap.workspace = true
 env_logger.workspace = true
 extension.workspace = true
 extension_host.workspace = true
@@ -69,6 +70,7 @@ libc.workspace = true
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }
 clock = { workspace = true, features = ["test-support"] }
+dap = { workspace = true, features = ["test-support"] }
 fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 http_client = { workspace = true, features = ["test-support"] }

crates/remote_server/src/headless_project.rs 🔗

@@ -1,5 +1,6 @@
 use ::proto::{FromProto, ToProto};
 use anyhow::{anyhow, Result};
+use dap::DapRegistry;
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
 use fs::Fs;
@@ -52,6 +53,7 @@ pub struct HeadlessAppState {
     pub http_client: Arc<dyn HttpClient>,
     pub node_runtime: NodeRuntime,
     pub languages: Arc<LanguageRegistry>,
+    pub debug_adapters: Arc<DapRegistry>,
     pub extension_host_proxy: Arc<ExtensionHostProxy>,
 }
 
@@ -69,6 +71,7 @@ impl HeadlessProject {
             http_client,
             node_runtime,
             languages,
+            debug_adapters,
             extension_host_proxy: proxy,
         }: HeadlessAppState,
         cx: &mut Context<Self>,
@@ -108,6 +111,7 @@ impl HeadlessProject {
                 node_runtime.clone(),
                 fs.clone(),
                 languages.clone(),
+                debug_adapters.clone(),
                 environment.clone(),
                 toolchain_store.read(cx).as_language_toolchain_store(),
                 breakpoint_store.clone(),

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -4,6 +4,7 @@
 use crate::headless_project::HeadlessProject;
 use client::{Client, UserStore};
 use clock::FakeSystemClock;
+use dap::DapRegistry;
 use extension::ExtensionHostProxy;
 use fs::{FakeFs, Fs};
 use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
@@ -1445,6 +1446,7 @@ pub async fn init_test(
     let http_client = Arc::new(BlockedHttpClient);
     let node_runtime = NodeRuntime::unavailable();
     let languages = Arc::new(LanguageRegistry::new(cx.executor()));
+    let debug_adapters = DapRegistry::default().into();
     let proxy = Arc::new(ExtensionHostProxy::new());
     server_cx.update(HeadlessProject::init);
     let headless = server_cx.new(|cx| {
@@ -1457,6 +1459,7 @@ pub async fn init_test(
                 http_client,
                 node_runtime,
                 languages,
+                debug_adapters,
                 extension_host_proxy: proxy,
             },
             cx,

crates/remote_server/src/unix.rs 🔗

@@ -3,6 +3,7 @@ use crate::HeadlessProject;
 use anyhow::{anyhow, Context as _, Result};
 use chrono::Utc;
 use client::{telemetry, ProxySettings};
+use dap::DapRegistry;
 use extension::ExtensionHostProxy;
 use fs::{Fs, RealFs};
 use futures::channel::mpsc;
@@ -471,6 +472,7 @@ pub fn execute_run(
             let mut languages = LanguageRegistry::new(cx.background_executor().clone());
             languages.set_language_server_download_dir(paths::languages_dir().clone());
             let languages = Arc::new(languages);
+            let debug_adapters = DapRegistry::default().into();
 
             HeadlessProject::new(
                 HeadlessAppState {
@@ -479,6 +481,7 @@ pub fn execute_run(
                     http_client,
                     node_runtime,
                     languages,
+                    debug_adapters,
                     extension_host_proxy,
                 },
                 cx,

crates/task/src/debug_format.rs 🔗

@@ -1,6 +1,6 @@
+use dap_types::StartDebuggingRequestArguments;
 use schemars::{gen::SchemaSettings, JsonSchema};
 use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
 use std::net::Ipv4Addr;
 use std::path::PathBuf;
 use util::ResultExt;
@@ -45,97 +45,121 @@ pub struct AttachConfig {
     pub process_id: Option<u32>,
 }
 
+/// Represents the launch request information of the debug adapter
+#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
+pub struct LaunchConfig {
+    /// The program that you trying to debug
+    pub program: String,
+    /// The current working directory of your project
+    pub cwd: Option<PathBuf>,
+}
+
 /// Represents the type that will determine which request to call on the debug adapter
-#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
-#[serde(rename_all = "lowercase")]
+#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
+#[serde(rename_all = "lowercase", untagged)]
 pub enum DebugRequestType {
     /// Call the `launch` request on the debug adapter
-    #[default]
-    Launch,
+    Launch(LaunchConfig),
     /// Call the `attach` request on the debug adapter
     Attach(AttachConfig),
 }
 
-/// The Debug adapter to use
-#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
-#[serde(rename_all = "lowercase", tag = "adapter")]
-pub enum DebugAdapterKind {
-    /// Manually setup starting a debug adapter
-    /// The argument within is used to start the DAP
-    Custom(CustomArgs),
-    /// Use debugpy
-    Python(TCPHost),
-    /// Use vscode-php-debug
-    Php(TCPHost),
-    /// Use vscode-js-debug
-    Javascript(TCPHost),
-    /// Use delve
-    Go(TCPHost),
-    /// Use lldb
-    Lldb,
-    /// Use GDB's built-in DAP support
-    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
-    Gdb,
-    /// Used for integration tests
-    #[cfg(any(test, feature = "test-support"))]
-    #[serde(skip)]
-    Fake((bool, dap_types::Capabilities)),
-}
-
-impl DebugAdapterKind {
-    /// Returns the display name for the adapter kind
-    pub fn display_name(&self) -> &str {
+/// Represents a request for starting the debugger.
+/// Contrary to `DebugRequestType`, `DebugRequestDisposition` is not Serializable.
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum DebugRequestDisposition {
+    /// Debug session configured by the user.
+    UserConfigured(DebugRequestType),
+    /// Debug session configured by the debug adapter
+    ReverseRequest(StartDebuggingRequestArguments),
+}
+
+impl DebugRequestDisposition {
+    /// Get the current working directory from request if it's a launch request and exits
+    pub fn cwd(&self) -> Option<PathBuf> {
         match self {
-            Self::Custom(_) => "Custom",
-            Self::Python(_) => "Python",
-            Self::Php(_) => "PHP",
-            Self::Javascript(_) => "JavaScript",
-            Self::Lldb => "LLDB",
-            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
-            Self::Gdb => "GDB",
-            Self::Go(_) => "Go",
-            #[cfg(any(test, feature = "test-support"))]
-            Self::Fake(_) => "Fake",
+            Self::UserConfigured(DebugRequestType::Launch(launch_config)) => {
+                launch_config.cwd.clone()
+            }
+            _ => None,
         }
     }
 }
-
-/// Custom arguments used to setup a custom debugger
-#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
-pub struct CustomArgs {
-    /// The connection that a custom debugger should use
-    #[serde(flatten)]
-    pub connection: DebugConnectionType,
-    /// The cli command used to start the debug adapter e.g. `python3`, `node` or the adapter binary
-    pub command: String,
-    /// The cli arguments used to start the debug adapter
-    pub args: Option<Vec<String>>,
-    /// The cli envs used to start the debug adapter
-    pub envs: Option<HashMap<String, String>>,
-}
-
 /// Represents the configuration for the debug adapter
-#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
-#[serde(rename_all = "snake_case")]
+#[derive(PartialEq, Eq, Clone, Debug)]
 pub struct DebugAdapterConfig {
     /// Name of the debug task
     pub label: String,
     /// The type of adapter you want to use
-    #[serde(flatten)]
-    pub kind: DebugAdapterKind,
+    pub adapter: String,
     /// The type of request that should be called on the debug adapter
-    #[serde(default)]
-    pub request: DebugRequestType,
-    /// The program that you trying to debug
-    pub program: Option<String>,
-    /// The current working directory of your project
-    pub cwd: Option<PathBuf>,
+    pub request: DebugRequestDisposition,
     /// Additional initialization arguments to be sent on DAP initialization
     pub initialize_args: Option<serde_json::Value>,
-    /// Whether the debug adapter supports attaching to a running process.
-    pub supports_attach: bool,
+    /// Optional TCP connection information
+    ///
+    /// If provided, this will be used to connect to the debug adapter instead of
+    /// spawning a new process. This is useful for connecting to a debug adapter
+    /// that is already running or is started by another process.
+    pub tcp_connection: Option<TCPHost>,
 }
 
+impl From<DebugTaskDefinition> for DebugAdapterConfig {
+    fn from(def: DebugTaskDefinition) -> Self {
+        Self {
+            label: def.label,
+            adapter: def.adapter,
+            request: DebugRequestDisposition::UserConfigured(def.request),
+            initialize_args: def.initialize_args,
+            tcp_connection: def.tcp_connection,
+        }
+    }
+}
+
+impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
+    type Error = ();
+    fn try_from(def: DebugAdapterConfig) -> Result<Self, Self::Error> {
+        let request = match def.request {
+            DebugRequestDisposition::UserConfigured(debug_request_type) => debug_request_type,
+            DebugRequestDisposition::ReverseRequest(_) => return Err(()),
+        };
+
+        Ok(Self {
+            label: def.label,
+            adapter: def.adapter,
+            request,
+            initialize_args: def.initialize_args,
+            tcp_connection: def.tcp_connection,
+        })
+    }
+}
+
+impl DebugTaskDefinition {
+    /// Translate from debug definition to a task template
+    pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
+        let command = "".to_string();
+
+        let cwd = if let DebugRequestType::Launch(ref launch) = self.request {
+            launch
+                .cwd
+                .as_ref()
+                .map(|path| path.to_string_lossy().into_owned())
+        } else {
+            None
+        };
+        let label = self.label.clone();
+        let task_type = TaskType::Debug(self);
+
+        Ok(TaskTemplate {
+            label,
+            command,
+            args: vec![],
+            task_type,
+            cwd,
+            ..Default::default()
+        })
+    }
+}
 /// Represents the type of the debugger adapter connection
 #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 #[serde(rename_all = "lowercase", tag = "connection")]
@@ -151,48 +175,20 @@ pub enum DebugConnectionType {
 #[serde(rename_all = "snake_case")]
 pub struct DebugTaskDefinition {
     /// The adapter to run
-    #[serde(flatten)]
-    kind: DebugAdapterKind,
+    pub adapter: String,
     /// The type of request that should be called on the debug adapter
-    #[serde(default)]
-    request: DebugRequestType,
+    #[serde(flatten)]
+    pub request: DebugRequestType,
     /// Name of the debug task
-    label: String,
-    /// Program to run the debugger on
-    program: Option<String>,
-    /// The current working directory of your project
-    cwd: Option<String>,
+    pub label: String,
     /// Additional initialization arguments to be sent on DAP initialization
-    initialize_args: Option<serde_json::Value>,
-}
-
-impl DebugTaskDefinition {
-    /// Translate from debug definition to a task template
-    pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
-        let command = "".to_string();
-        let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists());
-
-        let task_type = TaskType::Debug(DebugAdapterConfig {
-            label: self.label.clone(),
-            kind: self.kind,
-            request: self.request,
-            program: self.program,
-            cwd: cwd.clone(),
-            initialize_args: self.initialize_args,
-            supports_attach: true,
-        });
-
-        let args: Vec<String> = Vec::new();
-
-        Ok(TaskTemplate {
-            label: self.label,
-            command,
-            args,
-            task_type,
-            cwd: self.cwd,
-            ..Default::default()
-        })
-    }
+    pub initialize_args: Option<serde_json::Value>,
+    /// Optional TCP connection information
+    ///
+    /// If provided, this will be used to connect to the debug adapter instead of
+    /// spawning a new process. This is useful for connecting to a debug adapter
+    /// that is already running or is started by another process.
+    pub tcp_connection: Option<TCPHost>,
 }
 
 /// A group of Debug Tasks defined in a JSON file.

crates/task/src/lib.rs 🔗

@@ -15,8 +15,8 @@ use std::path::PathBuf;
 use std::str::FromStr;
 
 pub use debug_format::{
-    AttachConfig, CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType,
-    DebugRequestType, DebugTaskDefinition, DebugTaskFile, TCPHost,
+    AttachConfig, DebugAdapterConfig, DebugConnectionType, DebugRequestDisposition,
+    DebugRequestType, DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
 };
 pub use task_template::{
     HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType,
@@ -104,14 +104,20 @@ impl ResolvedTask {
     }
 
     /// Get the configuration for the debug adapter that should be used for this task.
-    pub fn resolved_debug_adapter_config(&self) -> Option<DebugAdapterConfig> {
+    pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskDefinition> {
         match self.original_task.task_type.clone() {
             TaskType::Script => None,
             TaskType::Debug(mut adapter_config) => {
                 if let Some(resolved) = &self.resolved {
                     adapter_config.label = resolved.label.clone();
-                    adapter_config.program = resolved.program.clone().or(adapter_config.program);
-                    adapter_config.cwd = resolved.cwd.clone().or(adapter_config.cwd);
+                    if let DebugRequestType::Launch(ref mut launch) = adapter_config.request {
+                        if let Some(program) = resolved.program.clone() {
+                            launch.program = program;
+                        }
+                        if let Some(cwd) = resolved.cwd.clone() {
+                            launch.cwd = Some(cwd);
+                        }
+                    }
                 }
 
                 Some(adapter_config)

crates/task/src/task_template.rs 🔗

@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
 use util::{truncate_and_remove_front, ResultExt};
 
 use crate::{
-    debug_format::DebugAdapterConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal,
+    DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, Shell, SpawnInTerminal,
     TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX,
 };
 
@@ -78,18 +78,18 @@ pub struct TaskTemplate {
 /// Represents the type of task that is being ran
 #[derive(Default, Deserialize, Serialize, Eq, PartialEq, JsonSchema, Clone, Debug)]
 #[serde(rename_all = "snake_case", tag = "type")]
-#[expect(clippy::large_enum_variant)]
+#[allow(clippy::large_enum_variant)]
 pub enum TaskType {
     /// Act like a typically task that runs commands
     #[default]
     Script,
     /// This task starts the debugger for a language
-    Debug(DebugAdapterConfig),
+    Debug(DebugTaskDefinition),
 }
 
 #[cfg(test)]
 mod deserialization_tests {
-    use crate::{DebugAdapterKind, TCPHost};
+    use crate::LaunchConfig;
 
     use super::*;
     use serde_json::json;
@@ -105,19 +105,20 @@ mod deserialization_tests {
 
     #[test]
     fn deserialize_task_type_debug() {
-        let adapter_config = DebugAdapterConfig {
+        let adapter_config = DebugTaskDefinition {
             label: "test config".into(),
-            kind: DebugAdapterKind::Python(TCPHost::default()),
-            request: crate::DebugRequestType::Launch,
-            program: Some("main".to_string()),
-            supports_attach: false,
-            cwd: None,
+            adapter: "Debugpy".into(),
+            request: crate::DebugRequestType::Launch(LaunchConfig {
+                program: "main".to_string(),
+                cwd: None,
+            }),
             initialize_args: None,
+            tcp_connection: None,
         };
         let json = json!({
             "label": "test config",
             "type": "debug",
-            "adapter": "python",
+            "adapter": "Debugpy",
             "program": "main",
             "supports_attach": false,
         });
@@ -272,9 +273,9 @@ impl TaskTemplate {
         let program = match &self.task_type {
             TaskType::Script => None,
             TaskType::Debug(adapter_config) => {
-                if let Some(program) = &adapter_config.program {
+                if let DebugRequestType::Launch(ref launch) = &adapter_config.request {
                     Some(substitute_all_template_variables_in_str(
-                        program,
+                        &launch.program,
                         &task_variables,
                         &variable_names,
                         &mut substituted_variables,

crates/tasks_ui/src/modal.rs 🔗

@@ -349,7 +349,7 @@ impl PickerDelegate for TasksModalDelegate {
                             _ => {
                                 project.update(cx, |project, cx| {
                                     project
-                                        .start_debug_session(config, cx)
+                                        .start_debug_session(config.into(), cx)
                                         .detach_and_log_err(cx);
                                 });
                             }
@@ -521,7 +521,10 @@ impl PickerDelegate for TasksModalDelegate {
                     // This would allow users to access to debug history and other issues
                     TaskType::Debug(_) => workspace.project().update(cx, |project, cx| {
                         project
-                            .start_debug_session(task.resolved_debug_adapter_config().unwrap(), cx)
+                            .start_debug_session(
+                                task.resolved_debug_adapter_config().unwrap().into(),
+                                cx,
+                            )
                             .detach_and_log_err(cx);
                     }),
                 };

crates/workspace/Cargo.toml 🔗

@@ -35,6 +35,7 @@ client.workspace = true
 clock.workspace = true
 collections.workspace = true
 component.workspace = true
+dap.workspace = true
 db.workspace = true
 derive_more.workspace = true
 fs.workspace = true
@@ -68,6 +69,7 @@ zed_actions.workspace = true
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }
 client = { workspace = true, features = ["test-support"] }
+dap = { workspace = true, features = ["test-support"] }
 db = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
 fs = { workspace = true, features = ["test-support"] }

crates/workspace/src/workspace.rs 🔗

@@ -14,6 +14,7 @@ mod toast_layer;
 mod toolbar;
 mod workspace_settings;
 
+use dap::DapRegistry;
 pub use toast_layer::{RunAction, ToastAction, ToastLayer, ToastView};
 
 use anyhow::{anyhow, Context as _, Result};
@@ -637,6 +638,7 @@ pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
 
 pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
+    pub debug_adapters: Arc<DapRegistry>,
     pub client: Arc<Client>,
     pub user_store: Entity<UserStore>,
     pub workspace_store: Entity<WorkspaceStore>,
@@ -688,6 +690,7 @@ impl AppState {
 
         let fs = fs::FakeFs::new(cx.background_executor().clone());
         let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
+        let debug_adapters = Arc::new(DapRegistry::fake());
         let clock = Arc::new(clock::FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();
         let client = Client::new(clock, http_client.clone(), cx);
@@ -703,6 +706,7 @@ impl AppState {
             client,
             fs,
             languages,
+            debug_adapters,
             user_store,
             workspace_store,
             node_runtime: NodeRuntime::unavailable(),
@@ -1197,6 +1201,7 @@ impl Workspace {
             app_state.node_runtime.clone(),
             app_state.user_store.clone(),
             app_state.languages.clone(),
+            app_state.debug_adapters.clone(),
             app_state.fs.clone(),
             env,
             cx,
@@ -5025,6 +5030,7 @@ impl Workspace {
         window.activate_window();
         let app_state = Arc::new(AppState {
             languages: project.read(cx).languages().clone(),
+            debug_adapters: project.read(cx).debug_adapters().clone(),
             workspace_store,
             client,
             user_store,

crates/zed/Cargo.toml 🔗

@@ -41,6 +41,8 @@ command_palette.workspace = true
 command_palette_hooks.workspace = true
 component_preview.workspace = true
 copilot.workspace = true
+dap.workspace = true
+dap_adapters.workspace = true
 debugger_ui.workspace = true
 debugger_tools.workspace = true
 db.workspace = true
@@ -149,6 +151,7 @@ ashpd.workspace = true
 
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }
+dap = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 image_viewer = { workspace = true, features = ["test-support"] }

crates/zed/src/main.rs 🔗

@@ -11,6 +11,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 use client::{parse_zed_link, Client, ProxySettings, UserStore};
 use collab_ui::channel_view::ChannelView;
 use collections::HashMap;
+use dap::DapRegistry;
 use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
 use editor::Editor;
 use extension::ExtensionHostProxy;
@@ -422,6 +423,7 @@ fn main() {
 
         let app_state = Arc::new(AppState {
             languages: languages.clone(),
+            debug_adapters: DapRegistry::default().into(),
             client: client.clone(),
             user_store: user_store.clone(),
             fs: fs.clone(),
@@ -433,6 +435,7 @@ fn main() {
         AppState::set_global(Arc::downgrade(&app_state), cx);
 
         auto_update::init(client.http_client(), cx);
+        dap_adapters::init(app_state.debug_adapters.clone());
         auto_update_ui::init(cx);
         reliability::init(
             client.http_client(),