1use anyhow::{Context as _, bail};
2use collections::HashMap;
3use dap::{
4 StartDebuggingRequestArguments,
5 adapters::{
6 DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
7 latest_github_release,
8 },
9};
10use fs::Fs;
11use gpui::{AsyncApp, SharedString};
12use language::LanguageName;
13use log::warn;
14use serde_json::{Map, Value};
15use task::TcpArgumentsTemplate;
16use util;
17
18use std::{
19 borrow::Cow,
20 env::consts,
21 ffi::OsStr,
22 path::{Path, PathBuf},
23 str::FromStr,
24 sync::{LazyLock, OnceLock},
25};
26
27use crate::*;
28
29#[derive(Default, Debug)]
30pub struct GoDebugAdapter {
31 shim_path: OnceLock<PathBuf>,
32}
33
34impl GoDebugAdapter {
35 pub const ADAPTER_NAME: &'static str = "Delve";
36 async fn fetch_latest_adapter_version(
37 delegate: &Arc<dyn DapDelegate>,
38 ) -> Result<AdapterVersion> {
39 let release = latest_github_release(
40 &"zed-industries/delve-shim-dap",
41 true,
42 false,
43 delegate.http_client(),
44 )
45 .await?;
46
47 let os = match consts::OS {
48 "macos" => "apple-darwin",
49 "linux" => "unknown-linux-gnu",
50 "windows" => "pc-windows-msvc",
51 other => bail!("Running on unsupported os: {other}"),
52 };
53 let suffix = if consts::OS == "windows" {
54 ".zip"
55 } else {
56 ".tar.gz"
57 };
58 let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
59 let asset = release
60 .assets
61 .iter()
62 .find(|asset| asset.name == asset_name)
63 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
64
65 Ok(AdapterVersion {
66 tag_name: release.tag_name,
67 url: asset.browser_download_url.clone(),
68 })
69 }
70 async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
71 if let Some(path) = self.shim_path.get().cloned() {
72 return Ok(path);
73 }
74
75 let asset = Self::fetch_latest_adapter_version(delegate).await?;
76 let ty = if consts::OS == "windows" {
77 DownloadedFileType::Zip
78 } else {
79 DownloadedFileType::GzipTar
80 };
81 download_adapter_from_github(
82 "delve-shim-dap".into(),
83 asset.clone(),
84 ty,
85 paths::debug_adapters_dir(),
86 delegate.as_ref(),
87 )
88 .await?;
89
90 let path = paths::debug_adapters_dir()
91 .join("delve-shim-dap")
92 .join(format!("delve-shim-dap_{}", asset.tag_name))
93 .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
94 self.shim_path.set(path.clone()).ok();
95
96 Ok(path)
97 }
98}
99
100#[cfg(feature = "update-schemas")]
101impl GoDebugAdapter {
102 pub fn get_schema(
103 temp_dir: &tempfile::TempDir,
104 delegate: UpdateSchemasDapDelegate,
105 ) -> anyhow::Result<serde_json::Value> {
106 let (package_json, package_nls_json) = get_vsix_package_json(
107 temp_dir,
108 "golang/vscode-go",
109 |version| {
110 let version = version
111 .tag_name
112 .strip_prefix("v")
113 .context("parse tag name")?;
114 Ok(format!("go-{version}.vsix"))
115 },
116 delegate,
117 )?;
118 let package_json = parse_package_json(package_json, package_nls_json)?;
119
120 let [debugger] =
121 <[_; 1]>::try_from(package_json.contributes.debuggers).map_err(|debuggers| {
122 anyhow::anyhow!("unexpected number of go debuggers: {}", debuggers.len())
123 })?;
124
125 Ok(schema_for_configuration_attributes(
126 debugger.configuration_attributes,
127 ))
128 }
129}
130
131#[async_trait(?Send)]
132impl DebugAdapter for GoDebugAdapter {
133 fn name(&self) -> DebugAdapterName {
134 DebugAdapterName(Self::ADAPTER_NAME.into())
135 }
136
137 fn adapter_language_name(&self) -> Option<LanguageName> {
138 Some(SharedString::new_static("Go").into())
139 }
140
141 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
142 static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
143 const RAW_SCHEMA: &str = include_str!("../schemas/Delve.json");
144 serde_json::from_str(RAW_SCHEMA).unwrap()
145 });
146 Cow::Borrowed(&*SCHEMA)
147 }
148
149 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
150 let mut args = match &zed_scenario.request {
151 dap::DebugRequest::Attach(attach_config) => {
152 json!({
153 "request": "attach",
154 "mode": "debug",
155 "processId": attach_config.process_id,
156 })
157 }
158 dap::DebugRequest::Launch(launch_config) => {
159 let mode = if launch_config.program != "." {
160 "exec"
161 } else {
162 "debug"
163 };
164
165 json!({
166 "request": "launch",
167 "mode": mode,
168 "program": launch_config.program,
169 "cwd": launch_config.cwd,
170 "args": launch_config.args,
171 "env": launch_config.env_json()
172 })
173 }
174 };
175
176 let map = args.as_object_mut().unwrap();
177
178 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
179 map.insert("stopOnEntry".into(), stop_on_entry.into());
180 }
181
182 Ok(DebugScenario {
183 adapter: zed_scenario.adapter,
184 label: zed_scenario.label,
185 build: None,
186 config: args,
187 tcp_connection: None,
188 })
189 }
190
191 async fn get_binary(
192 &self,
193 delegate: &Arc<dyn DapDelegate>,
194 task_definition: &DebugTaskDefinition,
195 user_installed_path: Option<PathBuf>,
196 user_args: Option<Vec<String>>,
197 _cx: &mut AsyncApp,
198 ) -> Result<DebugAdapterBinary> {
199 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
200 let dlv_path = adapter_path.join("dlv");
201
202 let delve_path = if let Some(path) = user_installed_path {
203 path.to_string_lossy().to_string()
204 } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
205 path.to_string_lossy().to_string()
206 } else if delegate.fs().is_file(&dlv_path).await {
207 dlv_path.to_string_lossy().to_string()
208 } else {
209 let go = delegate
210 .which(OsStr::new("go"))
211 .await
212 .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
213
214 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
215
216 let install_output = util::command::new_smol_command(&go)
217 .env("GO111MODULE", "on")
218 .env("GOBIN", &adapter_path)
219 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
220 .output()
221 .await?;
222
223 if !install_output.status.success() {
224 bail!(
225 "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'",
226 String::from_utf8_lossy(&install_output.stdout),
227 String::from_utf8_lossy(&install_output.stderr)
228 );
229 }
230
231 adapter_path.join("dlv").to_string_lossy().to_string()
232 };
233
234 let cwd = Some(
235 task_definition
236 .config
237 .get("cwd")
238 .and_then(|s| s.as_str())
239 .map(PathBuf::from)
240 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
241 );
242
243 let arguments;
244 let command;
245 let connection;
246
247 let mut configuration = task_definition.config.clone();
248 let mut envs = HashMap::default();
249
250 if let Some(configuration) = configuration.as_object_mut() {
251 configuration
252 .entry("cwd")
253 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
254
255 handle_envs(
256 configuration,
257 &mut envs,
258 cwd.as_deref(),
259 delegate.fs().clone(),
260 )
261 .await;
262 }
263
264 if let Some(connection_options) = &task_definition.tcp_connection {
265 command = None;
266 arguments = vec![];
267 let (host, port, timeout) =
268 crate::configure_tcp_connection(connection_options.clone()).await?;
269 connection = Some(TcpArguments {
270 host,
271 port,
272 timeout,
273 });
274 } else {
275 let minidelve_path = self.install_shim(delegate).await?;
276 let (host, port, _) =
277 crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
278 command = Some(minidelve_path.to_string_lossy().into_owned());
279 connection = None;
280 arguments = if let Some(mut args) = user_args {
281 args.insert(0, delve_path);
282 args
283 } else if cfg!(windows) {
284 vec![
285 delve_path,
286 "dap".into(),
287 "--listen".into(),
288 format!("{}:{}", host, port),
289 "--headless".into(),
290 ]
291 } else {
292 vec![
293 delve_path,
294 "dap".into(),
295 "--listen".into(),
296 format!("{}:{}", host, port),
297 ]
298 };
299 }
300 Ok(DebugAdapterBinary {
301 command,
302 arguments,
303 cwd,
304 envs,
305 connection,
306 request_args: StartDebuggingRequestArguments {
307 configuration,
308 request: self.request_kind(&task_definition.config).await?,
309 },
310 })
311 }
312}
313
314// delve doesn't do anything with the envFile setting, so we intercept it
315async fn handle_envs(
316 config: &mut Map<String, Value>,
317 envs: &mut HashMap<String, String>,
318 cwd: Option<&Path>,
319 fs: Arc<dyn Fs>,
320) -> Option<()> {
321 let env_files = match config.get("envFile")? {
322 Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
323 Value::String(s) => vec![Some(s.as_str())],
324 _ => return None,
325 };
326
327 let rebase_path = |path: PathBuf| {
328 if path.is_absolute() {
329 Some(path)
330 } else {
331 cwd.map(|p| p.join(path))
332 }
333 };
334
335 for path in env_files {
336 let Some(path) = path
337 .and_then(|s| PathBuf::from_str(s).ok())
338 .and_then(rebase_path)
339 else {
340 continue;
341 };
342
343 if let Ok(file) = fs.open_sync(&path).await {
344 envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
345 } else {
346 warn!("While starting Go debug session: failed to read env file {path:?}");
347 };
348 }
349
350 // remove envFile now that it's been handled
351 config.remove("entry");
352 Some(())
353}