Debug adapters log to console (#29957)

Conrad Irwin created

Closes #ISSUE

Release Notes:

- N/A

Change summary

Cargo.lock                                          |   1 
crates/activity_indicator/src/activity_indicator.rs |  13 -
crates/dap/src/adapters.rs                          | 110 -------------
crates/dap_adapters/Cargo.toml                      |   1 
crates/dap_adapters/src/codelldb.rs                 |  88 ++++++-----
crates/dap_adapters/src/dap_adapters.rs             |   6 
crates/dap_adapters/src/gdb.rs                      |  22 --
crates/dap_adapters/src/go.rs                       |  37 ----
crates/dap_adapters/src/javascript.rs               |  53 ++++--
crates/dap_adapters/src/php.rs                      |  51 ++++--
crates/dap_adapters/src/python.rs                   |  40 +++-
crates/language/src/language_registry.rs            |  12 -
crates/project/src/debugger/dap_store.rs            | 114 +++++++++-----
crates/project/src/debugger/session.rs              |  29 +++
crates/project/src/project.rs                       |   1 
crates/proto/proto/debugger.proto                   |   7 
crates/proto/proto/zed.proto                        |   4 
crates/proto/src/proto.rs                           |   2 
crates/remote_server/src/headless_project.rs        |   7 
19 files changed, 268 insertions(+), 330 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4100,6 +4100,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "dap",
+ "futures 0.3.31",
  "gpui",
  "language",
  "lsp-types",

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -84,19 +84,6 @@ impl ActivityIndicator {
             })
             .detach();
 
-            let mut status_events = languages.dap_server_binary_statuses();
-            cx.spawn(async move |this, cx| {
-                while let Some((name, status)) = status_events.next().await {
-                    this.update(cx, |this, cx| {
-                        this.statuses.retain(|s| s.name != name);
-                        this.statuses.push(ServerStatus { name, status });
-                        cx.notify();
-                    })?;
-                }
-                anyhow::Ok(())
-            })
-            .detach();
-
             cx.subscribe(
                 &project.read(cx).lsp_store(),
                 |_, _, event, cx| match event {

crates/dap/src/adapters.rs 🔗

@@ -12,10 +12,9 @@ use language::LanguageToolchainStore;
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use settings::WorktreeId;
-use smol::{self, fs::File, lock::Mutex};
+use smol::{self, fs::File};
 use std::{
     borrow::Borrow,
-    collections::HashSet,
     ffi::OsStr,
     fmt::Debug,
     net::Ipv4Addr,
@@ -24,7 +23,6 @@ use std::{
     sync::Arc,
 };
 use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
-use util::ResultExt;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum DapStatus {
@@ -41,8 +39,7 @@ pub trait DapDelegate {
     fn node_runtime(&self) -> NodeRuntime;
     fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
     fn fs(&self) -> Arc<dyn Fs>;
-    fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
-    fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
+    fn output_to_console(&self, msg: String);
     fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> collections::HashMap<String, String>;
 }
@@ -293,7 +290,7 @@ impl DebugAdapterBinary {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct AdapterVersion {
     pub tag_name: String,
     pub url: String,
@@ -335,6 +332,7 @@ pub async fn download_adapter_from_github(
         adapter_name,
         &github_version.url,
     );
+    delegate.output_to_console(format!("Downloading from {}...", github_version.url));
 
     let mut response = delegate
         .http_client()
@@ -418,81 +416,6 @@ pub trait DebugAdapter: 'static + Send + Sync {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         cx: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        if delegate
-            .updated_adapters()
-            .lock()
-            .await
-            .contains(&self.name())
-        {
-            log::info!("Using cached debug adapter binary {}", self.name());
-
-            if let Some(binary) = self
-                .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
-                .await
-                .log_err()
-            {
-                return Ok(binary);
-            }
-
-            log::info!(
-                "Cached binary {} is corrupt falling back to install",
-                self.name()
-            );
-        }
-
-        log::info!("Getting latest version of debug adapter {}", self.name());
-        delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
-        if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
-            log::info!("Installing latest version of debug adapter {}", self.name());
-            delegate.update_status(self.name(), DapStatus::Downloading);
-            match self.install_binary(version, delegate).await {
-                Ok(_) => {
-                    delegate.update_status(self.name(), DapStatus::None);
-                }
-                Err(error) => {
-                    delegate.update_status(
-                        self.name(),
-                        DapStatus::Failed {
-                            error: error.to_string(),
-                        },
-                    );
-
-                    return Err(error);
-                }
-            }
-
-            delegate
-                .updated_adapters()
-                .lock_arc()
-                .await
-                .insert(self.name());
-        }
-
-        self.get_installed_binary(delegate, &config, user_installed_path, cx)
-            .await
-    }
-
-    async fn fetch_latest_adapter_version(
-        &self,
-        delegate: &dyn DapDelegate,
-    ) -> Result<AdapterVersion>;
-
-    /// Installs the binary for the debug adapter.
-    /// This method is called when the adapter binary is not found or needs to be updated.
-    /// It should download and install the necessary files for the debug adapter to function.
-    async fn install_binary(
-        &self,
-        version: AdapterVersion,
-        delegate: &dyn DapDelegate,
-    ) -> Result<()>;
-
-    async fn get_installed_binary(
-        &self,
-        delegate: &dyn DapDelegate,
-        config: &DebugTaskDefinition,
-        user_installed_path: Option<PathBuf>,
-        cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary>;
 
     fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
@@ -561,29 +484,4 @@ impl DebugAdapter for FakeAdapter {
             request_args: self.request_args(config),
         })
     }
-
-    async fn fetch_latest_adapter_version(
-        &self,
-        _delegate: &dyn DapDelegate,
-    ) -> Result<AdapterVersion> {
-        unimplemented!("fetch latest adapter version");
-    }
-
-    async fn install_binary(
-        &self,
-        _version: AdapterVersion,
-        _delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        unimplemented!("install binary");
-    }
-
-    async fn get_installed_binary(
-        &self,
-        _: &dyn DapDelegate,
-        _: &DebugTaskDefinition,
-        _: Option<PathBuf>,
-        _: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        unimplemented!("get installed binary");
-    }
 }

crates/dap_adapters/Cargo.toml 🔗

@@ -24,6 +24,7 @@ doctest = false
 anyhow.workspace = true
 async-trait.workspace = true
 dap.workspace = true
+futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 lsp-types.workspace = true

crates/dap_adapters/src/codelldb.rs 🔗

@@ -1,16 +1,18 @@
 use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
 
-use anyhow::{Result, bail};
+use anyhow::Result;
 use async_trait::async_trait;
 use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
+use futures::StreamExt;
 use gpui::AsyncApp;
 use task::DebugRequest;
+use util::fs::remove_matching;
 
 use crate::*;
 
 #[derive(Default)]
 pub(crate) struct CodeLldbDebugAdapter {
-    last_known_version: OnceLock<String>,
+    path_to_codelldb: OnceLock<String>,
 }
 
 impl CodeLldbDebugAdapter {
@@ -54,29 +56,6 @@ impl CodeLldbDebugAdapter {
             configuration,
         }
     }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for CodeLldbDebugAdapter {
-    fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
-    }
-
-    async fn install_binary(
-        &self,
-        version: AdapterVersion,
-        delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        adapters::download_adapter_from_github(
-            self.name(),
-            version,
-            adapters::DownloadedFileType::Vsix,
-            delegate,
-        )
-        .await?;
-
-        Ok(())
-    }
 
     async fn fetch_latest_adapter_version(
         &self,
@@ -107,7 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
             }
         };
         let asset_name = format!("codelldb-{platform}-{arch}.vsix");
-        let _ = self.last_known_version.set(release.tag_name.clone());
         let ret = AdapterVersion {
             tag_name: release.tag_name,
             url: release
@@ -121,28 +99,56 @@ impl DebugAdapter for CodeLldbDebugAdapter {
 
         Ok(ret)
     }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for CodeLldbDebugAdapter {
+    fn name(&self) -> DebugAdapterName {
+        DebugAdapterName(Self::ADAPTER_NAME.into())
+    }
 
-    async fn get_installed_binary(
+    async fn get_binary(
         &self,
-        _: &dyn DapDelegate,
+        delegate: &dyn DapDelegate,
         config: &DebugTaskDefinition,
-        _: Option<PathBuf>,
+        user_installed_path: Option<PathBuf>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
-        let Some(version) = self.last_known_version.get() else {
-            bail!("Could not determine latest CodeLLDB version");
+        let mut command = user_installed_path
+            .map(|p| p.to_string_lossy().to_string())
+            .or(self.path_to_codelldb.get().cloned());
+
+        if command.is_none() {
+            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
+            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
+            let version_path =
+                if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
+                    adapters::download_adapter_from_github(
+                        self.name(),
+                        version.clone(),
+                        adapters::DownloadedFileType::Vsix,
+                        delegate,
+                    )
+                    .await?;
+                    let version_path =
+                        adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
+                    remove_matching(&adapter_path, |entry| entry != version_path).await;
+                    version_path
+                } else {
+                    let mut paths = delegate.fs().read_dir(&adapter_path).await?;
+                    paths
+                        .next()
+                        .await
+                        .ok_or_else(|| anyhow!("No adapter found"))??
+                };
+            let adapter_dir = version_path.join("extension").join("adapter");
+            let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
+            self.path_to_codelldb.set(path.clone()).ok();
+            command = Some(path);
         };
-        let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
-        let version_path = adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version));
-
-        let adapter_dir = version_path.join("extension").join("adapter");
-        let command = adapter_dir.join("codelldb");
-        let command = command
-            .to_str()
-            .map(ToOwned::to_owned)
-            .ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
+
         Ok(DebugAdapterBinary {
-            command,
+            command: command.unwrap(),
             cwd: None,
             arguments: vec![
                 "--settings".into(),

crates/dap_adapters/src/dap_adapters.rs 🔗

@@ -29,9 +29,9 @@ use task::TcpArgumentsTemplate;
 pub fn init(cx: &mut App) {
     cx.update_default_global(|registry: &mut DapRegistry, _cx| {
         registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
-        registry.add_adapter(Arc::from(PythonDebugAdapter));
-        registry.add_adapter(Arc::from(PhpDebugAdapter));
-        registry.add_adapter(Arc::from(JsDebugAdapter));
+        registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
+        registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
+        registry.add_adapter(Arc::from(JsDebugAdapter::default()));
         registry.add_adapter(Arc::from(GoDebugAdapter));
         registry.add_adapter(Arc::from(GdbDebugAdapter));
     })

crates/dap_adapters/src/gdb.rs 🔗

@@ -90,26 +90,4 @@ impl DebugAdapter for GdbDebugAdapter {
             request_args: self.request_args(config),
         })
     }
-
-    async fn install_binary(
-        &self,
-        _version: AdapterVersion,
-        _delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        unimplemented!("GDB debug adapter cannot be installed by Zed (yet)")
-    }
-
-    async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
-        unimplemented!("Fetch latest GDB version not implemented (yet)")
-    }
-
-    async fn get_installed_binary(
-        &self,
-        _: &dyn DapDelegate,
-        _: &DebugTaskDefinition,
-        _: Option<std::path::PathBuf>,
-        _: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        unimplemented!("GDB cannot be installed by Zed (yet)")
-    }
 }

crates/dap_adapters/src/go.rs 🔗

@@ -46,41 +46,8 @@ impl DebugAdapter for GoDebugAdapter {
         &self,
         delegate: &dyn DapDelegate,
         config: &DebugTaskDefinition,
-        user_installed_path: Option<PathBuf>,
-        cx: &mut AsyncApp,
-    ) -> Result<DebugAdapterBinary> {
-        self.get_installed_binary(delegate, config, user_installed_path, cx)
-            .await
-    }
-
-    async fn fetch_latest_adapter_version(
-        &self,
-        _delegate: &dyn DapDelegate,
-    ) -> Result<AdapterVersion> {
-        unimplemented!("This adapter is used from path for now");
-    }
-
-    async fn install_binary(
-        &self,
-        version: AdapterVersion,
-        delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        adapters::download_adapter_from_github(
-            self.name(),
-            version,
-            adapters::DownloadedFileType::Zip,
-            delegate,
-        )
-        .await?;
-        Ok(())
-    }
-
-    async fn get_installed_binary(
-        &self,
-        delegate: &dyn DapDelegate,
-        config: &DebugTaskDefinition,
-        _: Option<PathBuf>,
-        _: &mut AsyncApp,
+        _user_installed_path: Option<PathBuf>,
+        _cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let delve_path = delegate
             .which(OsStr::new("dlv"))

crates/dap_adapters/src/javascript.rs 🔗

@@ -1,13 +1,16 @@
 use adapters::latest_github_release;
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
-use std::{collections::HashMap, path::PathBuf};
+use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
 use task::DebugRequest;
+use util::ResultExt;
 
 use crate::*;
 
-#[derive(Debug)]
-pub(crate) struct JsDebugAdapter;
+#[derive(Debug, Default)]
+pub(crate) struct JsDebugAdapter {
+    checked: OnceLock<()>,
+}
 
 impl JsDebugAdapter {
     const ADAPTER_NAME: &'static str = "JavaScript";
@@ -47,13 +50,6 @@ impl JsDebugAdapter {
             request: config.request.to_dap(),
         }
     }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for JsDebugAdapter {
-    fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
-    }
 
     async fn fetch_latest_adapter_version(
         &self,
@@ -130,20 +126,35 @@ impl DebugAdapter for JsDebugAdapter {
             request_args: self.request_args(config),
         })
     }
+}
 
-    async fn install_binary(
+#[async_trait(?Send)]
+impl DebugAdapter for JsDebugAdapter {
+    fn name(&self) -> DebugAdapterName {
+        DebugAdapterName(Self::ADAPTER_NAME.into())
+    }
+
+    async fn get_binary(
         &self,
-        version: AdapterVersion,
         delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        adapters::download_adapter_from_github(
-            self.name(),
-            version,
-            adapters::DownloadedFileType::GzipTar,
-            delegate,
-        )
-        .await?;
+        config: &DebugTaskDefinition,
+        user_installed_path: Option<PathBuf>,
+        cx: &mut AsyncApp,
+    ) -> Result<DebugAdapterBinary> {
+        if self.checked.set(()).is_ok() {
+            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
+            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
+                adapters::download_adapter_from_github(
+                    self.name(),
+                    version,
+                    adapters::DownloadedFileType::GzipTar,
+                    delegate,
+                )
+                .await?;
+            }
+        }
 
-        return Ok(());
+        self.get_installed_binary(delegate, &config, user_installed_path, cx)
+            .await
     }
 }

crates/dap_adapters/src/php.rs 🔗

@@ -1,12 +1,15 @@
 use adapters::latest_github_release;
 use dap::adapters::{DebugTaskDefinition, TcpArguments};
 use gpui::AsyncApp;
-use std::{collections::HashMap, path::PathBuf};
+use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
+use util::ResultExt;
 
 use crate::*;
 
 #[derive(Default)]
-pub(crate) struct PhpDebugAdapter;
+pub(crate) struct PhpDebugAdapter {
+    checked: OnceLock<()>,
+}
 
 impl PhpDebugAdapter {
     const ADAPTER_NAME: &'static str = "PHP";
@@ -32,13 +35,6 @@ impl PhpDebugAdapter {
             }),
         }
     }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for PhpDebugAdapter {
-    fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
-    }
 
     async fn fetch_latest_adapter_version(
         &self,
@@ -114,20 +110,35 @@ impl DebugAdapter for PhpDebugAdapter {
             request_args: self.request_args(config)?,
         })
     }
+}
 
-    async fn install_binary(
+#[async_trait(?Send)]
+impl DebugAdapter for PhpDebugAdapter {
+    fn name(&self) -> DebugAdapterName {
+        DebugAdapterName(Self::ADAPTER_NAME.into())
+    }
+
+    async fn get_binary(
         &self,
-        version: AdapterVersion,
         delegate: &dyn DapDelegate,
-    ) -> Result<()> {
-        adapters::download_adapter_from_github(
-            self.name(),
-            version,
-            adapters::DownloadedFileType::Vsix,
-            delegate,
-        )
-        .await?;
+        config: &DebugTaskDefinition,
+        user_installed_path: Option<PathBuf>,
+        cx: &mut AsyncApp,
+    ) -> Result<DebugAdapterBinary> {
+        if self.checked.set(()).is_ok() {
+            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
+            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
+                adapters::download_adapter_from_github(
+                    self.name(),
+                    version,
+                    adapters::DownloadedFileType::Vsix,
+                    delegate,
+                )
+                .await?;
+            }
+        }
 
-        Ok(())
+        self.get_installed_binary(delegate, &config, user_installed_path, cx)
+            .await
     }
 }

crates/dap_adapters/src/python.rs 🔗

@@ -4,10 +4,13 @@ use dap::{
     adapters::InlineValueProvider,
 };
 use gpui::AsyncApp;
-use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
+use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
+use util::ResultExt;
 
 #[derive(Default)]
-pub(crate) struct PythonDebugAdapter;
+pub(crate) struct PythonDebugAdapter {
+    checked: OnceLock<()>,
+}
 
 impl PythonDebugAdapter {
     const ADAPTER_NAME: &'static str = "Debugpy";
@@ -46,14 +49,6 @@ impl PythonDebugAdapter {
             request: config.request.to_dap(),
         }
     }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for PythonDebugAdapter {
-    fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
-    }
-
     async fn fetch_latest_adapter_version(
         &self,
         delegate: &dyn DapDelegate,
@@ -162,6 +157,31 @@ impl DebugAdapter for PythonDebugAdapter {
             request_args: self.request_args(config),
         })
     }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for PythonDebugAdapter {
+    fn name(&self) -> DebugAdapterName {
+        DebugAdapterName(Self::ADAPTER_NAME.into())
+    }
+
+    async fn get_binary(
+        &self,
+        delegate: &dyn DapDelegate,
+        config: &DebugTaskDefinition,
+        user_installed_path: Option<PathBuf>,
+        cx: &mut AsyncApp,
+    ) -> Result<DebugAdapterBinary> {
+        if self.checked.set(()).is_ok() {
+            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
+            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
+                self.install_binary(version, delegate).await?;
+            }
+        }
+
+        self.get_installed_binary(delegate, &config, user_installed_path, cx)
+            .await
+    }
 
     fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
         Some(Box::new(PythonInlineValueProvider))

crates/language/src/language_registry.rs 🔗

@@ -104,7 +104,6 @@ pub struct LanguageRegistry {
     language_server_download_dir: Option<Arc<Path>>,
     executor: BackgroundExecutor,
     lsp_binary_status_tx: BinaryStatusSender,
-    dap_binary_status_tx: BinaryStatusSender,
 }
 
 struct LanguageRegistryState {
@@ -269,7 +268,6 @@ impl LanguageRegistry {
             }),
             language_server_download_dir: None,
             lsp_binary_status_tx: Default::default(),
-            dap_binary_status_tx: Default::default(),
             executor,
         };
         this.add(PLAIN_TEXT.clone());
@@ -986,10 +984,6 @@ impl LanguageRegistry {
         self.lsp_binary_status_tx.send(server_name.0, status);
     }
 
-    pub fn update_dap_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
-        self.dap_binary_status_tx.send(server_name.0, status);
-    }
-
     pub fn next_language_server_id(&self) -> LanguageServerId {
         self.state.write().next_language_server_id()
     }
@@ -1046,12 +1040,6 @@ impl LanguageRegistry {
         self.lsp_binary_status_tx.subscribe()
     }
 
-    pub fn dap_server_binary_statuses(
-        &self,
-    ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
-        self.dap_binary_status_tx.subscribe()
-    }
-
     pub async fn delete_server_container(&self, name: LanguageServerName) {
         log::info!("deleting server container");
         let Some(dir) = self.language_server_download_dir(&name) else {

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

@@ -16,22 +16,20 @@ use collections::HashMap;
 use dap::{
     Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
     EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
-    adapters::{
-        DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
-    },
+    adapters::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments},
     client::SessionId,
     messages::Message,
     requests::{Completions, Evaluate},
 };
 use fs::Fs;
-use futures::future::{Shared, join_all};
+use futures::{
+    StreamExt,
+    channel::mpsc::{self, UnboundedSender},
+    future::{Shared, join_all},
+};
 use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 use http_client::HttpClient;
-use language::{
-    BinaryStatus, Buffer, LanguageRegistry, LanguageToolchainStore,
-    language_settings::InlayHintKind, range_from_lsp,
-};
-use lsp::LanguageServerName;
+use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind, range_from_lsp};
 use node_runtime::NodeRuntime;
 
 use remote::SshRemoteClient;
@@ -40,10 +38,9 @@ use rpc::{
     proto::{self},
 };
 use settings::{Settings, WorktreeId};
-use smol::lock::Mutex;
 use std::{
     borrow::Borrow,
-    collections::{BTreeMap, HashSet},
+    collections::BTreeMap,
     ffi::OsStr,
     net::Ipv4Addr,
     path::{Path, PathBuf},
@@ -78,7 +75,6 @@ pub struct LocalDapStore {
     node_runtime: NodeRuntime,
     http_client: Arc<dyn HttpClient>,
     environment: Entity<ProjectEnvironment>,
-    language_registry: Arc<LanguageRegistry>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
 }
 
@@ -102,12 +98,13 @@ impl EventEmitter<DapStoreEvent> for DapStore {}
 impl DapStore {
     pub fn init(client: &AnyProtoClient, cx: &mut App) {
         static ADD_LOCATORS: Once = Once::new();
-        client.add_entity_request_handler(Self::handle_run_debug_locator);
-        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
         ADD_LOCATORS.call_once(|| {
             DapRegistry::global(cx)
                 .add_locator("cargo".into(), Arc::new(locators::cargo::CargoLocator {}))
         });
+        client.add_entity_request_handler(Self::handle_run_debug_locator);
+        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
+        client.add_entity_message_handler(Self::handle_log_to_debug_console);
     }
 
     #[expect(clippy::too_many_arguments)]
@@ -115,7 +112,6 @@ impl DapStore {
         http_client: Arc<dyn HttpClient>,
         node_runtime: NodeRuntime,
         fs: Arc<dyn Fs>,
-        language_registry: Arc<LanguageRegistry>,
         environment: Entity<ProjectEnvironment>,
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         worktree_store: Entity<WorktreeStore>,
@@ -128,7 +124,6 @@ impl DapStore {
             http_client,
             node_runtime,
             toolchain_store,
-            language_registry,
         });
 
         Self::new(mode, breakpoint_store, worktree_store, cx)
@@ -179,6 +174,8 @@ impl DapStore {
     pub fn get_debug_adapter_binary(
         &mut self,
         definition: DebugTaskDefinition,
+        session_id: SessionId,
+        console: UnboundedSender<String>,
         cx: &mut Context<Self>,
     ) -> Task<Result<DebugAdapterBinary>> {
         match &self.mode {
@@ -196,7 +193,7 @@ impl DapStore {
                     .get(&adapter.name())
                     .and_then(|s| s.binary.as_ref().map(PathBuf::from));
 
-                let delegate = self.delegate(&worktree, cx);
+                let delegate = self.delegate(&worktree, console, cx);
                 let cwd: Arc<Path> = definition
                     .cwd()
                     .unwrap_or(worktree.read(cx).abs_path().as_ref())
@@ -228,6 +225,7 @@ impl DapStore {
             }
             DapStoreMode::Ssh(ssh) => {
                 let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
+                    session_id: session_id.to_proto(),
                     project_id: ssh.upstream_project_id,
                     definition: Some(definition.to_proto()),
                 });
@@ -445,13 +443,15 @@ impl DapStore {
         };
 
         let dap_store = cx.weak_entity();
+        let console = session.update(cx, |session, cx| session.console_output(cx));
+        let session_id = session.read(cx).session_id();
 
         cx.spawn({
             let session = session.clone();
             async move |this, cx| {
                 let mut binary = this
                     .update(cx, |this, cx| {
-                        this.get_debug_adapter_binary(definition.clone(), cx)
+                        this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
                     })?
                     .await?;
 
@@ -522,7 +522,12 @@ impl DapStore {
         Ok(())
     }
 
-    fn delegate(&self, worktree: &Entity<Worktree>, cx: &mut App) -> DapAdapterDelegate {
+    fn delegate(
+        &self,
+        worktree: &Entity<Worktree>,
+        console: UnboundedSender<String>,
+        cx: &mut App,
+    ) -> DapAdapterDelegate {
         let Some(local_store) = self.as_local() else {
             unimplemented!("Starting session on remote side");
         };
@@ -530,9 +535,9 @@ impl DapStore {
         DapAdapterDelegate::new(
             local_store.fs.clone(),
             worktree.read(cx).id(),
+            console,
             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| {
                 env.get_worktree_environment(worktree.clone(), cx)
@@ -802,24 +807,65 @@ impl DapStore {
                 .definition
                 .ok_or_else(|| anyhow!("missing definition"))?,
         )?;
+        let (tx, mut rx) = mpsc::unbounded();
+        let session_id = envelope.payload.session_id;
+        cx.spawn({
+            let this = this.clone();
+            async move |cx| {
+                while let Some(message) = rx.next().await {
+                    this.update(cx, |this, _| {
+                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
+                            downstream
+                                .send(proto::LogToDebugConsole {
+                                    project_id,
+                                    session_id,
+                                    message,
+                                })
+                                .ok();
+                        }
+                    })
+                    .ok();
+                }
+            }
+        })
+        .detach();
+
         let binary = this
             .update(&mut cx, |this, cx| {
-                this.get_debug_adapter_binary(definition, cx)
+                this.get_debug_adapter_binary(definition, SessionId::from_proto(session_id), tx, cx)
             })?
             .await?;
         Ok(binary.to_proto())
     }
+
+    async fn handle_log_to_debug_console(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::LogToDebugConsole>,
+        mut cx: AsyncApp,
+    ) -> Result<()> {
+        let session_id = SessionId::from_proto(envelope.payload.session_id);
+        this.update(&mut cx, |this, cx| {
+            let Some(session) = this.sessions.get(&session_id) else {
+                return;
+            };
+            session.update(cx, |session, cx| {
+                session
+                    .console_output(cx)
+                    .unbounded_send(envelope.payload.message)
+                    .ok();
+            })
+        })
+    }
 }
 
 #[derive(Clone)]
 pub struct DapAdapterDelegate {
     fs: Arc<dyn Fs>,
+    console: mpsc::UnboundedSender<String>,
     worktree_id: WorktreeId,
     node_runtime: NodeRuntime,
     http_client: Arc<dyn HttpClient>,
-    language_registry: Arc<LanguageRegistry>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
-    updated_adapters: Arc<Mutex<HashSet<DebugAdapterName>>>,
     load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
 }
 
@@ -827,21 +873,20 @@ impl DapAdapterDelegate {
     pub fn new(
         fs: Arc<dyn Fs>,
         worktree_id: WorktreeId,
+        status: mpsc::UnboundedSender<String>,
         node_runtime: NodeRuntime,
         http_client: Arc<dyn HttpClient>,
-        language_registry: Arc<LanguageRegistry>,
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
     ) -> Self {
         Self {
             fs,
+            console: status,
             worktree_id,
             http_client,
             node_runtime,
             toolchain_store,
-            language_registry,
             load_shell_env_task,
-            updated_adapters: Default::default(),
         }
     }
 }
@@ -864,21 +909,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
         self.fs.clone()
     }
 
-    fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>> {
-        self.updated_adapters.clone()
-    }
-
-    fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) {
-        let name = SharedString::from(dap_name.to_string());
-        let status = match status {
-            DapStatus::None => BinaryStatus::None,
-            DapStatus::Downloading => BinaryStatus::Downloading,
-            DapStatus::Failed { error } => BinaryStatus::Failed { error },
-            DapStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
-        };
-
-        self.language_registry
-            .update_dap_status(LanguageServerName(name), status);
+    fn output_to_console(&self, msg: String) {
+        self.console.unbounded_send(msg).ok();
     }
 
     fn which(&self, command: &OsStr) -> Option<PathBuf> {

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

@@ -170,7 +170,7 @@ impl LocalMode {
             } else {
                 DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx.clone())
                     .await
-                    .with_context(|| "Failed to start communication with debug adapter")?
+                    .with_context(|| format!("Failed to start {:?}", &binary.command))?
             },
         );
 
@@ -815,6 +815,33 @@ impl Session {
         self.is_session_terminated
     }
 
+    pub fn console_output(&mut self, cx: &mut Context<Self>) -> mpsc::UnboundedSender<String> {
+        let (tx, mut rx) = mpsc::unbounded();
+
+        cx.spawn(async move |this, cx| {
+            while let Some(output) = rx.next().await {
+                this.update(cx, |this, _| {
+                    this.output_token.0 += 1;
+                    this.output.push_back(dap::OutputEvent {
+                        category: None,
+                        output,
+                        group: None,
+                        variables_reference: None,
+                        source: None,
+                        line: None,
+                        column: None,
+                        data: None,
+                        location_reference: None,
+                    });
+                })?;
+            }
+            anyhow::Ok(())
+        })
+        .detach();
+
+        return tx;
+    }
+
     pub fn is_local(&self) -> bool {
         matches!(self.mode, Mode::Running(_))
     }

crates/project/src/project.rs 🔗

@@ -894,7 +894,6 @@ impl Project {
                     client.http_client(),
                     node.clone(),
                     fs.clone(),
-                    languages.clone(),
                     environment.clone(),
                     toolchain_store.read(cx).as_language_toolchain_store(),
                     worktree_store.clone(),

crates/proto/proto/debugger.proto 🔗

@@ -559,6 +559,7 @@ message DapModuleId {
 
 message GetDebugAdapterBinary {
     uint64 project_id = 1;
+    uint64 session_id = 3;
     DebugTaskDefinition definition = 2;
 }
 
@@ -605,3 +606,9 @@ message SpawnInTerminal {
     map<string, string> env = 4;
     optional string cwd = 5;
 }
+
+message LogToDebugConsole {
+    uint64 project_id = 1;
+    uint64 session_id = 2;
+    string message = 3;
+}

crates/proto/proto/zed.proto 🔗

@@ -384,7 +384,9 @@ message Envelope {
         LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344;
         LspExtCancelFlycheck lsp_ext_cancel_flycheck = 345;
         LspExtRunFlycheck lsp_ext_run_flycheck = 346;
-        LspExtClearFlycheck lsp_ext_clear_flycheck = 347; // current max
+        LspExtClearFlycheck lsp_ext_clear_flycheck = 347;
+
+        LogToDebugConsole log_to_debug_console = 348; // current max
     }
 
     reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -305,6 +305,7 @@ messages!(
     (DebugAdapterBinary, Background),
     (RunDebugLocators, Background),
     (DebugRequest, Background),
+    (LogToDebugConsole, Background),
 );
 
 request_messages!(
@@ -591,6 +592,7 @@ entity_messages!(
     ToggleBreakpoint,
     RunDebugLocators,
     GetDebugAdapterBinary,
+    LogToDebugConsole,
 );
 
 entity_messages!(

crates/remote_server/src/headless_project.rs 🔗

@@ -106,17 +106,18 @@ impl HeadlessProject {
             cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone()));
 
         let dap_store = cx.new(|cx| {
-            DapStore::new_local(
+            let mut dap_store = DapStore::new_local(
                 http_client.clone(),
                 node_runtime.clone(),
                 fs.clone(),
-                languages.clone(),
                 environment.clone(),
                 toolchain_store.read(cx).as_language_toolchain_store(),
                 worktree_store.clone(),
                 breakpoint_store.clone(),
                 cx,
-            )
+            );
+            dap_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
+            dap_store
         });
 
         let git_store = cx.new(|cx| {