Debugger implementation (#13433)

###  DISCLAIMER

> As of 6th March 2025, debugger is still in development. We plan to
merge it behind a staff-only feature flag for staff use only, followed
by non-public release and then finally a public one (akin to how Git
panel release was handled). This is done to ensure the best experience
when it gets released.

### END OF DISCLAIMER 

**The current state of the debugger implementation:**


https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9


https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f

----

All the todo's are in the following channel, so it's easier to work on
this together:
https://zed.dev/channel/zed-debugger-11370

If you are on Linux, you can use the following command to join the
channel:
```cli
zed https://zed.dev/channel/zed-debugger-11370 
```

## Current Features

- Collab
  - Breakpoints
    - Sync when you (re)join a project
    - Sync when you add/remove a breakpoint
  - Sync active debug line
  - Stack frames
    - Click on stack frame
      - View variables that belong to the stack frame
      - Visit the source file
    - Restart stack frame (if adapter supports this)
  - Variables
  - Loaded sources
  - Modules
  - Controls
    - Continue
    - Step back
      - Stepping granularity (configurable)
    - Step into
      - Stepping granularity (configurable)
    - Step over
      - Stepping granularity (configurable)
    - Step out
      - Stepping granularity (configurable)
  - Debug console
- Breakpoints
  - Log breakpoints
  - line breakpoints
  - Persistent between zed sessions (configurable)
  - Multi buffer support
  - Toggle disable/enable all breakpoints
- Stack frames
  - Click on stack frame
    - View variables that belong to the stack frame
    - Visit the source file
    - Show collapsed stack frames
  - Restart stack frame (if adapter supports this)
- Loaded sources
  - View all used loaded sources if supported by adapter.
- Modules
  - View all used modules (if adapter supports this)
- Variables
  - Copy value
  - Copy name
  - Copy memory reference
  - Set value (if adapter supports this)
  - keyboard navigation
- Debug Console
  - See logs
  - View output that was sent from debug adapter
    - Output grouping
  - Evaluate code
    - Updates the variable list
    - Auto completion
- If not supported by adapter, we will show auto-completion for existing
variables
- Debug Terminal
- Run custom commands and change env values right inside your Zed
terminal
- Attach to process (if adapter supports this)
  - Process picker
- Controls
  - Continue
  - Step back
    - Stepping granularity (configurable)
  - Step into
    - Stepping granularity (configurable)
  - Step over
    - Stepping granularity (configurable)
  - Step out
    - Stepping granularity (configurable)
  - Disconnect
  - Restart
  - Stop
- Warning when a debug session exited without hitting any breakpoint
- Debug view to see Adapter/RPC log messages
- Testing
  - Fake debug adapter
    - Fake requests & events

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
Remco Smits 2025-03-18 17:55:25 +01:00 committed by GitHub
parent ed4e654fdf
commit 41a60ffecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 25840 additions and 451 deletions

View file

@ -0,0 +1,41 @@
[package]
name = "dap_adapters"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = [
"dap/test-support",
"gpui/test-support",
"task/test-support",
"util/test-support",
]
[lints]
workspace = true
[lib]
path = "src/dap_adapters.rs"
doctest = false
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
dap.workspace = true
gpui.workspace = true
language.workspace = true
paths.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sysinfo.workspace = true
task.workspace = true
util.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -0,0 +1,84 @@
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})
}
}

View file

@ -0,0 +1,67 @@
mod custom;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod gdb;
mod go;
mod javascript;
mod lldb;
mod php;
mod python;
use std::{collections::HashMap, sync::Arc};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use custom::CustomDebugAdapter;
use dap::adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
use javascript::JsDebugAdapter;
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};
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 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<_>>(),
}
}

View file

@ -0,0 +1,83 @@
use std::ffi::OsStr;
use anyhow::Result;
use async_trait::async_trait;
use gpui::AsyncApp;
use task::DebugAdapterConfig;
use crate::*;
pub(crate) struct GdbDebugAdapter {}
impl GdbDebugAdapter {
const ADAPTER_NAME: &'static str = "gdb";
pub(crate) fn new() -> Self {
GdbDebugAdapter {}
}
}
#[async_trait(?Send)]
impl DebugAdapter for GdbDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
user_installed_path: Option<std::path::PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let user_setting_path = user_installed_path
.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()))
.ok_or(anyhow!("Could not find gdb in path"));
if gdb_path.is_err() && user_setting_path.is_none() {
bail!("Could not find gdb path or it's not installed");
}
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
Ok(DebugAdapterBinary {
command: gdb_path,
arguments: Some(vec!["-i=dap".into()]),
envs: None,
cwd: config.cwd.clone(),
connection: None,
})
}
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,
_: &DebugAdapterConfig,
_: Option<std::path::PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
unimplemented!("GDB cannot be installed by Zed (yet)")
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({"program": config.program, "cwd": config.cwd})
}
}

View file

@ -0,0 +1,100 @@
use dap::transport::TcpTransport;
use gpui::AsyncApp;
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
use crate::*;
pub(crate) struct GoDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
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,
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
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: &DebugAdapterConfig,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
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"))?;
Ok(DebugAdapterBinary {
command: delve_path,
arguments: Some(vec![
"dap".into(),
"--listen".into(),
format!("{}:{}", self.host, self.port).into(),
]),
cwd: config.cwd.clone(),
envs: None,
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
}),
})
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"cwd": config.cwd,
"subProcess": true,
})
}
}

View file

@ -0,0 +1,148 @@
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 crate::*;
pub(crate) struct JsDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
impl JsDebugAdapter {
const ADAPTER_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)]
impl DebugAdapter for JsDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "microsoft", Self::ADAPTER_NAME),
true,
false,
delegate.http_client(),
)
.await?;
let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
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(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
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 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!("Couldn't find JavaScript dap directory"))?
};
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
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,
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
}),
})
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::GzipTar,
delegate,
)
.await?;
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,
"type": "pwa-node",
"request": match config.request {
DebugRequestType::Launch => "launch",
DebugRequestType::Attach(_) => "attach",
},
"processId": pid,
"cwd": config.cwd,
})
}
}

View file

@ -0,0 +1,104 @@
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use anyhow::Result;
use async_trait::async_trait;
use gpui::AsyncApp;
use sysinfo::{Pid, Process};
use task::{DebugAdapterConfig, DebugRequestType};
use crate::*;
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<_>>()
}
}
#[async_trait(?Send)]
impl DebugAdapter for LldbDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let lldb_dap_path = if let Some(user_installed_path) = user_installed_path {
user_installed_path.to_string_lossy().into()
} else if cfg!(target_os = "macos") {
util::command::new_smol_command("xcrun")
.args(&["-f", "lldb-dap"])
.output()
.await
.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"))?
} 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(DebugAdapterBinary {
command: lldb_dap_path,
arguments: None,
envs: None,
cwd: config.cwd.clone(),
connection: None,
})
}
async fn install_binary(
&self,
_version: AdapterVersion,
_delegate: &dyn DapDelegate,
) -> Result<()> {
unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
}
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
unimplemented!("Fetch latest adapter version not implemented for lldb (yet)")
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugAdapterConfig,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
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,
"request": match config.request {
DebugRequestType::Launch => "launch",
DebugRequestType::Attach(_) => "attach",
},
"pid": pid,
"cwd": config.cwd,
})
}
}

View file

@ -0,0 +1,123 @@
use adapters::latest_github_release;
use dap::{adapters::TcpArguments, transport::TcpTransport};
use gpui::AsyncApp;
use std::{net::Ipv4Addr, path::PathBuf};
use crate::*;
pub(crate) struct PhpDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
impl PhpDebugAdapter {
const ADAPTER_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)]
impl DebugAdapter for PhpDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_NAME),
true,
false,
delegate.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(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
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 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!("Couldn't find PHP dap directory"))?
};
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: Some(vec![
adapter_path.join(Self::ADAPTER_PATH).into(),
format!("--server={}", self.port).into(),
]),
connection: Some(TcpArguments {
port: self.port,
host: self.host,
timeout: self.timeout,
}),
cwd: config.cwd.clone(),
envs: None,
})
}
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(())
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"cwd": config.cwd,
})
}
}

View file

@ -0,0 +1,142 @@
use crate::*;
use dap::transport::TcpTransport;
use gpui::AsyncApp;
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
pub(crate) struct PythonDebugAdapter {
port: u16,
host: Ipv4Addr,
timeout: Option<u64>,
}
impl PythonDebugAdapter {
const ADAPTER_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)]
impl DebugAdapter for PythonDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let github_repo = GithubRepo {
repo_name: Self::ADAPTER_NAME.into(),
repo_owner: "microsoft".into(),
};
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
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
{
// TODO Debugger: Rename folder instead of moving all files to another folder
// We're doing unnecessary IO work right now
util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
.await?;
}
Ok(())
}
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
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 toolchain = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
)
.await;
let python_path = if let Some(toolchain) = toolchain {
Some(toolchain.path.to_string())
} else {
BINARY_NAMES
.iter()
.filter_map(|cmd| {
delegate
.which(OsStr::new(cmd))
.map(|path| path.to_string_lossy().to_string())
})
.find(|_| true)
};
Ok(DebugAdapterBinary {
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(),
]),
connection: Some(adapters::TcpArguments {
host: self.host,
port: self.port,
timeout: self.timeout,
}),
cwd: config.cwd.clone(),
envs: None,
})
}
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
json!({
"program": config.program,
"subProcess": true,
"cwd": config.cwd,
"redirectOutput": true,
})
}
}