use anyhow::{Context as _, bail};use dap::{ StartDebuggingRequestArguments, adapters::{ DebugTaskDefinition, DownloadedFileType, download_adapter_from_github, latest_github_release, },}; use gpui::{AsyncApp, SharedString};use language::LanguageName;use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};use util; use crate::*; #[derive(Default, Debug)]pub(crate) struct GoDebugAdapter { shim_path: OnceLock<PathBuf>,} impl GoDebugAdapter { const ADAPTER_NAME: &'static str = "Delve"; async fn fetch_latest_adapter_version( delegate: &Arc<dyn DapDelegate>, ) -> Result<AdapterVersion> { let release = latest_github_release( &"zed-industries/delve-shim-dap", true, false, delegate.http_client(), ) .await?; let os = match consts::OS { "macos" => "apple-darwin", "linux" => "unknown-linux-gnu", "windows" => "pc-windows-msvc", other => bail!("Running on unsupported os: {other}"), }; let suffix = if consts::OS == "windows" { ".zip" } else { ".tar.gz" }; let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH); let asset = release .assets .iter() .find(|asset| asset.name == asset_name) .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; Ok(AdapterVersion { tag_name: release.tag_name, url: asset.browser_download_url.clone(), }) } async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> { if let Some(path) = self.shim_path.get().cloned() { return Ok(path); } let asset = Self::fetch_latest_adapter_version(delegate).await?; let ty = if consts::OS == "windows" { DownloadedFileType::Zip } else { DownloadedFileType::GzipTar }; download_adapter_from_github( "delve-shim-dap".into(), asset.clone(), ty, delegate.as_ref(), ) .await?; let path = paths::debug_adapters_dir() .join("delve-shim-dap") .join(format!("delve-shim-dap_{}", asset.tag_name)) .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX)); self.shim_path.set(path.clone()).ok(); Ok(path) }} #[async_trait(?Send)]impl DebugAdapter for GoDebugAdapter { fn name(&self) -> DebugAdapterName { DebugAdapterName(Self::ADAPTER_NAME.into()) } fn adapter_language_name(&self) -> Option<LanguageName> { Some(SharedString::new_static("Go").into()) } async fn dap_schema(&self) -> serde_json::Value { // Create common properties shared between launch and attach let common_properties = json!({ "debugAdapter": { "enum": ["legacy", "dlv-dap"], "description": "Select which debug adapter to use with this configuration.", "default": "dlv-dap" }, "stopOnEntry": { "type": "boolean", "description": "Automatically stop program after launch or attach.", "default": false }, "showLog": { "type": "boolean", "description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.", "default": false }, "cwd": { "type": "string", "description": "Workspace relative or absolute path to the working directory of the program being debugged.", "default": "${ZED_WORKTREE_ROOT}" }, "dlvFlags": { "type": "array", "description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.", "items": { "type": "string" }, "default": [] }, "port": { "type": "number", "description": "Debug server port. For remote configurations, this is where to connect.", "default": 2345 }, "host": { "type": "string", "description": "Debug server host. For remote configurations, this is where to connect.", "default": "127.0.0.1" }, "substitutePath": { "type": "array", "items": { "type": "object", "properties": { "from": { "type": "string", "description": "The absolute local path to be replaced." }, "to": { "type": "string", "description": "The absolute remote path to replace with." } } }, "description": "Mappings from local to remote paths for debugging.", "default": [] }, "trace": { "type": "string", "enum": ["verbose", "trace", "log", "info", "warn", "error"], "default": "error", "description": "Debug logging level." }, "backend": { "type": "string", "enum": ["default", "native", "lldb", "rr"], "description": "Backend used by delve. Maps to `dlv`'s `--backend` flag." }, "logOutput": { "type": "string", "enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"], "description": "Components that should produce debug output.", "default": "debugger" }, "logDest": { "type": "string", "description": "Log destination for delve." }, "stackTraceDepth": { "type": "number", "description": "Maximum depth of stack traces.", "default": 50 }, "showGlobalVariables": { "type": "boolean", "default": false, "description": "Show global package variables in variables pane." }, "showRegisters": { "type": "boolean", "default": false, "description": "Show register variables in variables pane." }, "hideSystemGoroutines": { "type": "boolean", "default": false, "description": "Hide system goroutines from call stack view." }, "console": { "default": "internalConsole", "description": "Where to launch the debugger.", "enum": ["internalConsole", "integratedTerminal"] }, "asRoot": { "default": false, "description": "Debug with elevated permissions (on Unix).", "type": "boolean" } }); // Create launch-specific properties let launch_properties = json!({ "program": { "type": "string", "description": "Path to the program folder or file to debug.", "default": "${ZED_WORKTREE_ROOT}" }, "args": { "type": ["array", "string"], "description": "Command line arguments for the program.", "items": { "type": "string" }, "default": [] }, "env": { "type": "object", "description": "Environment variables for the debugged program.", "default": {} }, "envFile": { "type": ["string", "array"], "items": { "type": "string" }, "description": "Path(s) to files with environment variables.", "default": "" }, "buildFlags": { "type": ["string", "array"], "items": { "type": "string" }, "description": "Flags for the Go compiler.", "default": [] }, "output": { "type": "string", "description": "Output path for the binary.", "default": "debug" }, "mode": { "enum": [ "debug", "test", "exec", "replay", "core"], "description": "Debug mode for launch configuration.", }, "traceDirPath": { "type": "string", "description": "Directory for record trace (for 'replay' mode).", "default": "" }, "coreFilePath": { "type": "string", "description": "Path to core dump file (for 'core' mode).", "default": "" } }); // Create attach-specific properties let attach_properties = json!({ "processId": { "anyOf": [ { "enum": ["${command:pickProcess}", "${command:pickGoProcess}"], "description": "Use process picker to select a process." }, { "type": "string", "description": "Process name to attach to." }, { "type": "number", "description": "Process ID to attach to." } ], "default": 0 }, "mode": { "enum": ["local", "remote"], "description": "Local or remote debugging.", "default": "local" }, "remotePath": { "type": "string", "description": "Path to source on remote machine.", "markdownDeprecationMessage": "Use `substitutePath` instead.", "default": "" } }); // Create the final schema json!({ "oneOf": [ { "allOf": [ { "type": "object", "required": ["request"], "properties": { "request": { "type": "string", "enum": ["launch"], "description": "Request to launch a new process" } } }, { "type": "object", "properties": common_properties }, { "type": "object", "required": ["program", "mode"], "properties": launch_properties } ] }, { "allOf": [ { "type": "object", "required": ["request"], "properties": { "request": { "type": "string", "enum": ["attach"], "description": "Request to attach to an existing process" } } }, { "type": "object", "properties": common_properties }, { "type": "object", "required": ["processId", "mode"], "properties": attach_properties } ] } ] }) } fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { let mut args = match &zed_scenario.request { dap::DebugRequest::Attach(attach_config) => { json!({ "request": "attach", "mode": "debug", "processId": attach_config.process_id, }) } dap::DebugRequest::Launch(launch_config) => { let mode = if launch_config.program != "." { "exec" } else { "debug" }; json!({ "request": "launch", "mode": mode, "program": launch_config.program, "cwd": launch_config.cwd, "args": launch_config.args, "env": launch_config.env_json() }) } }; let map = args.as_object_mut().unwrap(); if let Some(stop_on_entry) = zed_scenario.stop_on_entry { map.insert("stopOnEntry".into(), stop_on_entry.into()); } Ok(DebugScenario { adapter: zed_scenario.adapter, label: zed_scenario.label, build: None, config: args, tcp_connection: None, }) } async fn get_binary( &self, delegate: &Arc<dyn DapDelegate>, task_definition: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, _cx: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME); let dlv_path = adapter_path.join("dlv"); let delve_path = if let Some(path) = user_installed_path { path.to_string_lossy().to_string() } else if let Some(path) = delegate.which(OsStr::new("dlv")).await { path.to_string_lossy().to_string() } else if delegate.fs().is_file(&dlv_path).await { dlv_path.to_string_lossy().to_string() } else { let go = delegate .which(OsStr::new("go")) .await .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?; let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME); let install_output = util::command::new_smol_command(&go) .env("GO111MODULE", "on") .env("GOBIN", &adapter_path) .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"]) .output() .await?; if !install_output.status.success() { bail!( "failed to install dlv via `go install`. stdout: {:?}, stderr: {:?}\n Please try installing it manually using 'go install github.com/go-delve/delve/cmd/dlv@latest'", String::from_utf8_lossy(&install_output.stdout), String::from_utf8_lossy(&install_output.stderr) ); } adapter_path.join("dlv").to_string_lossy().to_string() }; let minidelve_path = self.install_shim(delegate).await?; let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?; let cwd = task_definition .config .get("cwd") .and_then(|s| s.as_str()) .map(PathBuf::from) .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()); let arguments = if cfg!(windows) { vec![ delve_path, "dap".into(), "--listen".into(), format!("{}:{}", host, port), "--headless".into(), ] } else { vec![ delve_path, "dap".into(), "--listen".into(), format!("{}:{}", host, port), ] }; Ok(DebugAdapterBinary { command: minidelve_path.to_string_lossy().into_owned(), arguments, cwd: Some(cwd), envs: HashMap::default(), connection: None, request_args: StartDebuggingRequestArguments { configuration: task_definition.config.clone(), request: self.request_kind(&task_definition.config)?, }, }) }}
use anyhow::{Context as _, bail};
use dap::{
StartDebuggingRequestArguments,
adapters::{
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
latest_github_release,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util;
use crate::*;
#[derive(Default, Debug)]
pub(crate) struct GoDebugAdapter {
shim_path: OnceLock<PathBuf>,
}
impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve";
async fn fetch_latest_adapter_version(
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&"zed-industries/delve-shim-dap",
true,
false,
delegate.http_client(),
)
.await?;
let os = match consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
"windows" => "pc-windows-msvc",
other => bail!("Running on unsupported os: {other}"),
};
let suffix = if consts::OS == "windows" {
".zip"
} else {
".tar.gz"
};
let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
Ok(AdapterVersion {
tag_name: release.tag_name,
url: asset.browser_download_url.clone(),
})
}
async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
if let Some(path) = self.shim_path.get().cloned() {
return Ok(path);
}
let asset = Self::fetch_latest_adapter_version(delegate).await?;
let ty = if consts::OS == "windows" {
DownloadedFileType::Zip
} else {
DownloadedFileType::GzipTar
};
download_adapter_from_github(
"delve-shim-dap".into(),
asset.clone(),
ty,
delegate.as_ref(),
)
.await?;
let path = paths::debug_adapters_dir()
.join("delve-shim-dap")
.join(format!("delve-shim-dap_{}", asset.tag_name))
.join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
self.shim_path.set(path.clone()).ok();
Ok(path)
}
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
async fn dap_schema(&self) -> serde_json::Value {
// Create common properties shared between launch and attach
let common_properties = json!({
"debugAdapter": {
"enum": ["legacy", "dlv-dap"],
"description": "Select which debug adapter to use with this configuration.",
"default": "dlv-dap"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch or attach.",
"default": false
},
"showLog": {
"type": "boolean",
"description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
"default": false
},
"cwd": {
"type": "string",
"description": "Workspace relative or absolute path to the working directory of the program being debugged.",
"default": "${ZED_WORKTREE_ROOT}"
},
"dlvFlags": {
"type": "array",
"description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
"items": {
"type": "string"
},
"default": []
},
"port": {
"type": "number",
"description": "Debug server port. For remote configurations, this is where to connect.",
"default": 2345
},
"host": {
"type": "string",
"description": "Debug server host. For remote configurations, this is where to connect.",
"default": "127.0.0.1"
},
"substitutePath": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "The absolute local path to be replaced."
},
"to": {
"type": "string",
"description": "The absolute remote path to replace with."
}
}
},
"description": "Mappings from local to remote paths for debugging.",
"default": []
},
"trace": {
"type": "string",
"enum": ["verbose", "trace", "log", "info", "warn", "error"],
"default": "error",
"description": "Debug logging level."
},
"backend": {
"type": "string",
"enum": ["default", "native", "lldb", "rr"],
"description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
},
"logOutput": {
"type": "string",
"enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
"description": "Components that should produce debug output.",
"default": "debugger"
},
"logDest": {
"type": "string",
"description": "Log destination for delve."
},
"stackTraceDepth": {
"type": "number",
"description": "Maximum depth of stack traces.",
"default": 50
},
"showGlobalVariables": {
"type": "boolean",
"default": false,
"description": "Show global package variables in variables pane."
},
"showRegisters": {
"type": "boolean",
"default": false,
"description": "Show register variables in variables pane."
},
"hideSystemGoroutines": {
"type": "boolean",
"default": false,
"description": "Hide system goroutines from call stack view."
},
"console": {
"default": "internalConsole",
"description": "Where to launch the debugger.",
"enum": ["internalConsole", "integratedTerminal"]
},
"asRoot": {
"default": false,
"description": "Debug with elevated permissions (on Unix).",
"type": "boolean"
}
});
// Create launch-specific properties
let launch_properties = json!({
"program": {
"type": "string",
"description": "Path to the program folder or file to debug.",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments for the program.",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program.",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Path(s) to files with environment variables.",
"default": ""
},
"buildFlags": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Flags for the Go compiler.",
"default": []
},
"output": {
"type": "string",
"description": "Output path for the binary.",
"default": "debug"
},
"mode": {
"enum": [ "debug", "test", "exec", "replay", "core"],
"description": "Debug mode for launch configuration.",
},
"traceDirPath": {
"type": "string",
"description": "Directory for record trace (for 'replay' mode).",
"default": ""
},
"coreFilePath": {
"type": "string",
"description": "Path to core dump file (for 'core' mode).",
"default": ""
}
});
// Create attach-specific properties
let attach_properties = json!({
"processId": {
"anyOf": [
{
"enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
"description": "Use process picker to select a process."
},
{
"type": "string",
"description": "Process name to attach to."
},
{
"type": "number",
"description": "Process ID to attach to."
}
],
"default": 0
},
"mode": {
"enum": ["local", "remote"],
"description": "Local or remote debugging.",
"default": "local"
},
"remotePath": {
"type": "string",
"description": "Path to source on remote machine.",
"markdownDeprecationMessage": "Use `substitutePath` instead.",
"default": ""
}
});
// Create the final schema
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["program", "mode"],
"properties": launch_properties
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["processId", "mode"],
"properties": attach_properties
}
]
}
]
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => {
json!({
"request": "attach",
"mode": "debug",
"processId": attach_config.process_id,
})
}
dap::DebugRequest::Launch(launch_config) => {
let mode = if launch_config.program != "." {
"exec"
} else {
"debug"
};
json!({
"request": "launch",
"mode": mode,
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json()
})
}
};
let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let dlv_path = adapter_path.join("dlv");
let delve_path = if let Some(path) = user_installed_path {
path.to_string_lossy().to_string()
} else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
path.to_string_lossy().to_string()
} else if delegate.fs().is_file(&dlv_path).await {
dlv_path.to_string_lossy().to_string()
} else {
let go = delegate
.which(OsStr::new("go"))
.await
.context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let install_output = util::command::new_smol_command(&go)
.env("GO111MODULE", "on")
.env("GOBIN", &adapter_path)
.args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
.output()
.await?;
if !install_output.status.success() {
bail!(
"failed to install dlv via `go install`. stdout: {:?}, stderr: {:?}\n Please try installing it manually using 'go install github.com/go-delve/delve/cmd/dlv@latest'",
String::from_utf8_lossy(&install_output.stdout),
String::from_utf8_lossy(&install_output.stderr)
);
}
adapter_path.join("dlv").to_string_lossy().to_string()
};
let minidelve_path = self.install_shim(delegate).await?;
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
let cwd = task_definition
.config
.get("cwd")
.and_then(|s| s.as_str())
.map(PathBuf::from)
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
let arguments = if cfg!(windows) {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
"--headless".into(),
]
} else {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
]
};
Ok(DebugAdapterBinary {
command: minidelve_path.to_string_lossy().into_owned(),
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
connection: None,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.request_kind(&task_definition.config)?,
},
})
}
}