diff --git a/Cargo.lock b/Cargo.lock index 05858ebce0480a..c165b80b92f3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,6 +3431,8 @@ name = "dap" version = "0.1.0" dependencies = [ "anyhow", + "async-compression", + "async-tar", "async-trait", "collections", "dap-types", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index bfddab846f475f..2097bea26a6ac0 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,6 +10,8 @@ workspace = true [dependencies] anyhow.workspace = true +async-compression.workspace = true +async-tar.workspace = true async-trait.workspace = true collections.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 3da1f5e87a88aa..d6fc9696564d2e 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,10 +1,14 @@ use crate::transport::Transport; use ::fs::Fs; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use async_trait::async_trait; +use futures::io::BufReader; use gpui::SharedString; -use http_client::{github::latest_github_release, HttpClient}; +pub use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; use serde_json::Value; use smol::{self, fs::File, lock::Mutex, process}; use std::{ @@ -17,6 +21,7 @@ use std::{ }; use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; +use util::ResultExt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum DapStatus { @@ -37,7 +42,7 @@ pub trait DapDelegate { async fn shell_env(&self) -> collections::HashMap; } -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] pub struct DebugAdapterName(pub Arc); impl Deref for DebugAdapterName { @@ -72,13 +77,18 @@ impl From for SharedString { } } +impl<'a> From<&'a str> for DebugAdapterName { + fn from(str: &'a str) -> DebugAdapterName { + DebugAdapterName(str.to_string().into()) + } +} + #[derive(Debug, Clone)] pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, pub cwd: Option, - pub version: String, } pub struct AdapterVersion { @@ -86,6 +96,12 @@ pub struct AdapterVersion { pub url: String, } +pub enum DownloadedFileType { + Vsix, + GzipTar, + Zip, +} + pub struct GithubRepo { pub repo_name: String, pub repo_owner: String, @@ -94,89 +110,79 @@ pub struct GithubRepo { pub async fn download_adapter_from_github( adapter_name: DebugAdapterName, github_version: AdapterVersion, + file_type: DownloadedFileType, delegate: &dyn DapDelegate, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(&adapter_name); - let version_dir = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); + let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); let fs = delegate.fs(); - let http_client = delegate - .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; + if version_path.exists() { + return Ok(version_path); } - if version_dir.exists() { - return Ok(version_dir); + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()) + .await + .context("Failed creating adapter path")?; } - let asset_name = format!("{}_{}.zip", &adapter_name, github_version.tag_name); - let zip_path = adapter_path.join(&asset_name); - fs.remove_file( - zip_path.as_path(), - fs::RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; + log::debug!( + "Downloading adapter {} from {}", + adapter_name, + &github_version.url, + ); + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; let mut response = http_client .get(&github_version.url, Default::default(), true) .await .context("Error downloading release")?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; - - let old_files: HashSet<_> = util::fs::collect_matching(&adapter_path.as_path(), |file_path| { - file_path != zip_path.as_path() - }) - .await - .into_iter() - .filter_map(|file_path| { - file_path - .file_name() - .and_then(|f| f.to_str()) - .map(|f| f.to_string()) - }) - .collect(); - - let _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + match file_type { + DownloadedFileType::GzipTar => { + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&version_path).await?; + } + DownloadedFileType::Zip | DownloadedFileType::Vsix => { + let zip_path = version_path.with_extension("zip"); + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence` + process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&version_path) + .output() + .await?; + + util::fs::remove_matching(&adapter_path, |entry| { + entry + .file_name() + .is_some_and(|file| file.to_string_lossy().ends_with(".zip")) + }) + .await; + } + } - let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| { - !file_name.ends_with(".zip") && !old_files.contains(file_name) + // remove older versions + util::fs::remove_matching(&adapter_path, |entry| { + entry.to_string_lossy() != version_path.to_string_lossy() }) - .await - .ok_or_else(|| anyhow!("Unzipped directory not found")); - - let file_name = file_name?; - let downloaded_path = adapter_path - .join(format!("{}_{}", adapter_name, github_version.tag_name)) - .to_owned(); - - fs.rename( - file_name.as_path(), - downloaded_path.as_path(), - Default::default(), - ) - .await?; + .await; - util::fs::remove_matching(&adapter_path, |entry| entry != version_dir).await; - - // if !unzip_status.success() { - // dbg!(unzip_status); - // Err(anyhow!("failed to unzip downloaded dap archive"))?; - // } - - Ok(downloaded_path) + Ok(version_path) } pub async fn fetch_latest_adapter_version_from_github( @@ -186,8 +192,14 @@ pub async fn fetch_latest_adapter_version_from_github( let http_client = delegate .http_client() .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name); - let release = latest_github_release(&repo_name_with_owner, false, false, http_client).await?; + + let release = latest_github_release( + &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), + false, + false, + http_client, + ) + .await?; Ok(AdapterVersion { tag_name: release.tag_name, @@ -203,39 +215,8 @@ pub trait DebugAdapter: 'static + Send + Sync { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - adapter_path: Option, + user_installed_path: Option, ) -> Result { - if let Some(adapter_path) = adapter_path { - if adapter_path.exists() { - log::info!( - "Using adapter path from settings\n debug adapter name: {}\n adapter_path: {:?}", - self.name(), - &adapter_path, - ); - - let binary = self - .get_installed_binary(delegate, &config, Some(adapter_path)) - .await; - - if binary.is_ok() { - return binary; - } else { - log::info!( - "Failed to get debug adapter path from user's setting.\n adapter_name: {}", - self.name() - ); - } - } else { - log::warn!( - r#"User downloaded adapter path does not exist - Debug Adapter: {}, - User Adapter Path: {:?}"#, - self.name(), - &adapter_path - ) - } - } - if delegate .updated_adapters() .lock() @@ -244,49 +225,39 @@ pub trait DebugAdapter: 'static + Send + Sync { { log::info!("Using cached debug adapter binary {}", self.name()); - return self.get_installed_binary(delegate, &config, None).await; + if let Some(binary) = self + .get_installed_binary(delegate, &config, user_installed_path.clone()) + .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); - let version = self.fetch_latest_adapter_version(delegate).await.ok(); - - let mut binary = self.get_installed_binary(delegate, &config, None).await; - - if let Some(version) = version { - if binary - .as_ref() - .is_ok_and(|binary| binary.version == version.tag_name) - { - delegate - .updated_adapters() - .lock_arc() - .await - .insert(self.name()); - - return binary; - } - + if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { + log::info!( + "Installiing latest version of debug adapter {}", + self.name() + ); delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; - binary = self.get_installed_binary(delegate, &config, None).await; - } else { - log::error!( - "Failed getting latest version of debug adapter {}", - self.name() - ); + delegate + .updated_adapters() + .lock_arc() + .await + .insert(self.name()); } - let binary = binary?; - - delegate - .updated_adapters() - .lock_arc() + self.get_installed_binary(delegate, &config, user_installed_path) .await - .insert(self.name()); - - Ok(binary) } fn transport(&self) -> Box; diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 5bcc371f82b6e5..c06107e8dcf2af 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -38,6 +38,7 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, sequence_count: AtomicU64, + binary: DebugAdapterBinary, executor: BackgroundExecutor, adapter: Arc>, transport_delegate: TransportDelegate, @@ -49,12 +50,14 @@ impl DebugAdapterClient { id: DebugAdapterClientId, config: DebugAdapterConfig, adapter: Arc>, + binary: DebugAdapterBinary, cx: &AsyncAppContext, ) -> Self { let transport_delegate = TransportDelegate::new(adapter.transport()); Self { id, + binary, adapter, transport_delegate, sequence_count: AtomicU64::new(1), @@ -196,6 +199,10 @@ impl DebugAdapterClient { &self.adapter } + pub fn binary(&self) -> &DebugAdapterBinary { + &self.binary + } + pub fn adapter_id(&self) -> String { self.adapter.name().to_string() } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 3a58d02469a3be..30a21dd7d404d2 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,4 +1,4 @@ -use std::ffi::OsString; +use std::{ffi::OsString, path::PathBuf}; use dap::transport::{StdioTransport, TcpTransport, Transport}; use serde_json::Value; @@ -54,7 +54,6 @@ impl DebugAdapter for CustomDebugAdapter { .map(|args| args.iter().map(OsString::from).collect()), cwd: config.cwd.clone(), envs: self.custom_args.envs.clone(), - version: "Custom Debug Adapter".to_string(), }) } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 357eb6075be6d4..4fddcd31f2520c 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -22,7 +22,6 @@ use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; use serde_json::{json, Value}; -use std::path::PathBuf; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index d87c1b0d927f3f..4f7ffcf7f621a8 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -31,7 +31,6 @@ impl DebugAdapter for GdbDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - user_installed_path: Option, ) -> Result { let user_setting_path = user_installed_path .filter(|p| p.exists()) @@ -74,7 +73,6 @@ impl DebugAdapter for GdbDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, - _: Option, ) -> Result { unimplemented!("GDB cannot be installed by Zed (yet)") } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index ae0973eafe17b5..587526eec9f056 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -36,9 +36,9 @@ impl DebugAdapter for GoDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - adapter_path: Option, + user_installed_path: Option, ) -> Result { - self.get_installed_binary(delegate, config, adapter_path) + self.get_installed_binary(delegate, config, user_installed_path) .await } @@ -46,12 +46,6 @@ impl DebugAdapter for GoDebugAdapter { &self, _delegate: &dyn DapDelegate, ) -> Result { - // let github_repo = GithubRepo { - // repo_name: Self::ADAPTER_NAME.into(), - // repo_owner: "go-delve".into(), - // }; - - // adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await unimplemented!("This adapter is used from path for now"); } @@ -60,7 +54,13 @@ impl DebugAdapter for GoDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - adapters::download_adapter_from_github(self.name(), version, delegate).await?; + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Zip, + delegate, + ) + .await?; Ok(()) } @@ -68,26 +68,30 @@ impl DebugAdapter for GoDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - _user_installed_path: Option, + _: Option, ) -> Result { let delve_path = delegate .which(OsStr::new("dlv")) .and_then(|p| p.to_str().map(|p| p.to_string())) .ok_or(anyhow!("Dlv not found in path"))?; - let ip_address = format!("{}:{}", self.host, self.port); - let version = "N/A".into(); - Ok(DebugAdapterBinary { command: delve_path, - arguments: Some(vec!["dap".into(), "--listen".into(), ip_address.into()]), + arguments: Some(vec![ + "dap".into(), + "--listen".into(), + format!("{}:{}", self.host, self.port).into(), + ]), cwd: config.cwd.clone(), envs: None, - version, }) } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program, "subProcess": true}) + json!({ + "program": config.program, + "cwd": config.cwd, + "subProcess": true, + }) } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 6e99e1dd4d50ad..805913fc510244 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,9 +1,9 @@ +use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; use regex::Regex; -use std::{collections::HashMap, net::Ipv4Addr}; +use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf}; use sysinfo::{Pid, Process}; use task::DebugRequestType; -use util::maybe; use crate::*; @@ -15,7 +15,7 @@ pub(crate) struct JsDebugAdapter { impl JsDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-js-debug"; - const ADAPTER_PATH: &'static str = "src/dapDebugServer.js"; + const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js"; pub(crate) async fn new(host: TCPHost) -> Result { Ok(JsDebugAdapter { @@ -40,12 +40,29 @@ impl DebugAdapter for JsDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_NAME.into(), - repo_owner: "microsoft".to_string(), - }; + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; + let release = latest_github_release( + &format!("{}/{}", "microsoft", Self::ADAPTER_NAME), + true, + false, + http_client, + ) + .await?; + + let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name); - adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + Ok(AdapterVersion { + tag_name: release.tag_name, + url: release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? + .browser_download_url + .clone(), + }) } async fn get_installed_binary( @@ -54,37 +71,24 @@ impl DebugAdapter for JsDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; + 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()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let adapter_path = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? - .to_string(); - - Ok((adapter_path, version)) - }) - .await; + let file_name_prefix = format!("{}_", self.name()); - let (adapter_path, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))? }; + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + Ok(DebugAdapterBinary { command: node_runtime .binary_path() @@ -94,10 +98,10 @@ impl DebugAdapter for JsDebugAdapter { arguments: Some(vec![ adapter_path.join(Self::ADAPTER_PATH).into(), self.port.to_string().into(), + self.host.to_string().into(), ]), cwd: config.cwd.clone(), envs: None, - version, }) } @@ -106,22 +110,13 @@ impl DebugAdapter for JsDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - let adapter_path = - adapters::download_adapter_from_github(self.name(), version, delegate).await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["compile"]) - .await - .ok(); + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::GzipTar, + delegate, + ) + .await?; return Ok(()); } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index e7587787644276..8a6bb9ad8b1d10 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,4 +1,4 @@ -use std::ffi::OsStr; +use std::{ffi::OsStr, path::PathBuf}; use anyhow::Result; use async_trait::async_trait; @@ -33,10 +33,6 @@ impl DebugAdapter for LldbDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let user_setting_path = user_installed_path - .filter(|p| p.exists()) - .and_then(|p| p.to_str().map(|s| s.to_string())); - let lldb_dap_path = if cfg!(target_os = "macos") { std::process::Command::new("xcrun") .args(&["-f", "lldb-dap"]) @@ -44,26 +40,21 @@ impl DebugAdapter for LldbDebugAdapter { .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|path| path.trim().to_string()) - .ok_or(anyhow!("Failed to find lldb-dap in user's path")) + .ok_or(anyhow!("Failed to find lldb-dap in user's path"))? + } else if let Some(user_installed_path) = user_installed_path { + user_installed_path.to_string_lossy().into() } else { delegate .which(OsStr::new("lldb-dap")) .and_then(|p| p.to_str().map(|s| s.to_string())) - .ok_or(anyhow!("Could not find lldb-dap in path")) + .ok_or(anyhow!("Could not find lldb-dap in path"))? }; - if lldb_dap_path.is_err() && user_setting_path.is_none() { - bail!("Could not find lldb-dap path or it's not installed"); - } - - let lldb_dap_path = user_setting_path.unwrap_or(lldb_dap_path?); - Ok(DebugAdapterBinary { command: lldb_dap_path, arguments: None, envs: None, cwd: config.cwd.clone(), - version: "1".into(), }) } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index eaea7be7141f10..db031900150752 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,6 +1,6 @@ +use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; -use std::net::Ipv4Addr; -use util::maybe; +use std::{net::Ipv4Addr, path::PathBuf}; use crate::*; @@ -12,7 +12,7 @@ pub(crate) struct PhpDebugAdapter { impl PhpDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-php-debug"; - const ADAPTER_PATH: &'static str = "out/phpDebug.js"; + const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; pub(crate) async fn new(host: TCPHost) -> Result { Ok(PhpDebugAdapter { @@ -37,12 +37,29 @@ impl DebugAdapter for PhpDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_NAME.into(), - repo_owner: "xdebug".into(), - }; - - adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; + let release = latest_github_release( + &format!("{}/{}", "xdebug", Self::ADAPTER_NAME), + true, + false, + http_client, + ) + .await?; + + let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", "")); + + Ok(AdapterVersion { + tag_name: release.tag_name, + url: release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? + .browser_download_url + .clone(), + }) } async fn get_installed_binary( @@ -51,37 +68,24 @@ impl DebugAdapter for PhpDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; + 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()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let adapter_path = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? - .to_string(); - - Ok((adapter_path, version)) - }) - .await; + let file_name_prefix = format!("{}_", self.name()); - let (adapter_path, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))? }; + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + Ok(DebugAdapterBinary { command: node_runtime .binary_path() @@ -94,7 +98,6 @@ impl DebugAdapter for PhpDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, - version, }) } @@ -103,22 +106,13 @@ impl DebugAdapter for PhpDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - let adapter_path = - adapters::download_adapter_from_github(self.name(), version, delegate).await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .is_ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["build"]) - .await - .is_ok(); + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Vsix, + delegate, + ) + .await?; Ok(()) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 855fa3551b6d30..38e1be293aca66 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,6 +1,5 @@ use dap::transport::{TcpTransport, Transport}; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; -use util::maybe; use crate::*; @@ -50,7 +49,25 @@ impl DebugAdapter for PythonDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - adapters::download_adapter_from_github(self.name(), version, delegate).await?; + let version_path = adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Zip, + delegate, + ) + .await?; + + // only needed when you install the latest version for the first time + if let Some(debugpy_dir) = + util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| { + file_name.starts_with("microsoft-debugpy-") + }) + .await + { + util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) + .await?; + } + Ok(()) } @@ -60,31 +77,17 @@ impl DebugAdapter for PythonDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let debugpy_dir = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Debugpy directory not found"))?; - - let version = debugpy_dir - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? - .to_string(); - - Ok((debugpy_dir, version)) - }) - .await; - - let (debugpy_dir, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + 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()); + + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Debugpy directory not found"))? }; let python_cmds = [ @@ -114,7 +117,6 @@ impl DebugAdapter for PythonDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, - version, }) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 1825682a9ba983..007ae83b99b626 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,9 +1,10 @@ -use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; +use crate::project_settings::ProjectSettings; +use crate::{ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; -use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; +use dap::adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, messages::{Message, Response}, @@ -37,7 +38,7 @@ use lsp::LanguageServerName; use node_runtime::NodeRuntime; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use serde_json::Value; -use settings::{Settings, WorktreeId}; +use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; use std::{ collections::{BTreeMap, HashSet}, @@ -51,7 +52,7 @@ use std::{ }; use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::{maybe, merge_json_value_into, ResultExt as _}; +use util::{merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -379,102 +380,55 @@ impl DapStore { } } - pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { - let Some(local_store) = self.as_local_mut() else { + pub fn start_client( + &mut self, + adapter: Arc>, + binary: DebugAdapterBinary, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) { + let Some(_) = self.as_local() else { return; }; - let mut adapter_delegate = local_store.delegate.clone(); - let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); - adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { - env.get_environment(None, worktree_abs_path, cx) - })); - let adapter_delegate = Arc::new(adapter_delegate); + if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { + cx.emit(DapStoreEvent::Notification( + "Debug adapter does not support `attach` request".into(), + )); + + return; + } let client_id = self.next_client_id(); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); - let client = maybe!(async { - let adapter = Arc::new( - build_adapter(&config.kind) - .await - .context("Creating debug adapter")?, - ); - - if !adapter.supports_attach() - && matches!(config.request, DebugRequestType::Attach(_)) - { - return Err(anyhow!("Debug adapter does not support `attach` request")); - } - - let path = cx.update(|cx| { - let name = LanguageServerName::from(adapter.name().as_ref()); - - ProjectSettings::get_global(cx) - .dap - .get(&name) - .and_then(|s| s.path.as_ref().map(PathBuf::from)) - })?; - - let binary = match adapter - .get_binary(adapter_delegate.as_ref(), &config, path) - .await - { - Err(error) => { - adapter_delegate.update_status( - adapter.name(), - DapStatus::Failed { - error: error.to_string(), - }, - ); - - return Err(error); - } - Ok(mut binary) => { - adapter_delegate.update_status(adapter.name(), DapStatus::None); - - let shell_env = adapter_delegate.shell_env().await; - let mut envs = binary.envs.unwrap_or_default(); - envs.extend(shell_env); - binary.envs = Some(envs); - - binary - } - }; - - let mut client = DebugAdapterClient::new(client_id, config, adapter, &cx); - - client - .start( - &binary, - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - }, - &mut cx, - ) - .await?; - - log::info!("Client has started"); - anyhow::Ok(client) - }) - .await; + let mut client = + DebugAdapterClient::new(client_id, config, adapter, binary.clone(), &cx); + + let result = client + .start( + &binary, + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + }, + &mut cx, + ) + .await; - let client = match client { - Err(error) => { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err()?; + if let Err(error) = result { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err()?; + return None; + } - return None; - } - Ok(client) => Arc::new(client), - }; + let client = Arc::new(client); this.update(&mut cx, |store, cx| { let handle = store @@ -499,6 +453,67 @@ impl DapStore { ); } + pub fn start_client_from_debug_config( + &mut self, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) { + let Some(local_store) = self.as_local_mut() else { + return; + }; + + let mut adapter_delegate = local_store.delegate.clone(); + let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); + adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { + env.get_environment(None, worktree_abs_path, cx) + })); + let adapter_delegate = Arc::new(adapter_delegate); + + cx.spawn(|this, mut cx| async move { + let adapter = Arc::new(build_adapter(&config.kind).await?); + + let binary = cx.update(|cx| { + let name = DebugAdapterName::from(adapter.name().as_ref()); + + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.binary.as_ref().map(PathBuf::from)) + })?; + + let (adapter, binary) = match adapter + .get_binary(adapter_delegate.as_ref(), &config, binary) + .await + { + Err(error) => { + adapter_delegate.update_status( + adapter.name(), + DapStatus::Failed { + error: error.to_string(), + }, + ); + + return Err(error); + } + Ok(mut binary) => { + adapter_delegate.update_status(adapter.name(), DapStatus::None); + + let shell_env = adapter_delegate.shell_env().await; + let mut envs = binary.envs.unwrap_or_default(); + envs.extend(shell_env); + binary.envs = Some(envs); + + (adapter, binary) + } + }; + + this.update(&mut cx, |store, cx| { + store.start_client(adapter, binary, config, cx); + }) + }) + .detach_and_log_err(cx); + } + pub fn initialize( &mut self, client_id: &DebugAdapterClientId, @@ -737,6 +752,8 @@ impl DapStore { let config = client.config(); store.start_client( + client.adapter().clone(), + client.binary().clone(), DebugAdapterConfig { kind: config.kind.clone(), request: match args.request { @@ -771,9 +788,7 @@ impl DapStore { command: StartDebugging::COMMAND.to_string(), body: Some(serde_json::to_value(ErrorResponse { error: None })?), })) - .await?; - - Ok(()) + .await } }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65b469e7c63ebc..76c54d28ae25c9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1268,7 +1268,7 @@ impl Project { ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { self.dap_store.update(cx, |store, cx| { - store.start_client(adapter_config, cx); + store.start_client_from_debug_config(adapter_config, cx); }); } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 8cec28c06ea03a..b712d1104249df 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1,5 +1,6 @@ use anyhow::Context; use collections::HashMap; +use dap::adapters::DebugAdapterName; use fs::Fs; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext}; use lsp::LanguageServerName; @@ -41,8 +42,9 @@ pub struct ProjectSettings { #[serde(default)] pub lsp: HashMap, + /// Configuration for Debugger-related features #[serde(default)] - pub dap: HashMap, + pub dap: HashMap, /// Configuration for Git-related features #[serde(default)] @@ -61,6 +63,12 @@ pub struct ProjectSettings { pub session: SessionSettings, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DapSettings { + pub binary: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct NodeBinarySettings { /// The path to the node binary @@ -184,12 +192,6 @@ pub struct LspSettings { pub settings: Option, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DapSettings { - pub path: Option, -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettings { /// Whether or not to restore unsaved buffers on restart. diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 5fe9b055ab3828..d6baf9364b7016 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,4 +1,5 @@ use crate::ResultExt; +use anyhow::{bail, Result}; use async_fs as fs; use futures_lite::StreamExt; use std::path::{Path, PathBuf}; @@ -56,9 +57,9 @@ where if let Some(file_name) = entry_path .file_name() - .and_then(|file_name| file_name.to_str()) + .map(|file_name| file_name.to_string_lossy()) { - if predicate(file_name) { + if predicate(&file_name) { return Some(entry_path); } } @@ -68,3 +69,25 @@ where None } + +pub async fn move_folder_files_to_folder>( + source_path: P, + target_path: P, +) -> Result<()> { + if !target_path.as_ref().is_dir() { + bail!("Folder not found or is not a directory"); + } + + let mut entries = fs::read_dir(source_path.as_ref()).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + let old_path = entry.path(); + let new_path = target_path.as_ref().join(entry.file_name()); + + fs::rename(&old_path, &new_path).await?; + } + + fs::remove_dir(source_path).await?; + + Ok(()) +}