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: &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 let configuration_attributes = debugger.configuration_attributes;
126 let conjuncts = configuration_attributes
127 .launch
128 .map(|schema| ("launch", schema))
129 .into_iter()
130 .chain(
131 configuration_attributes
132 .attach
133 .map(|schema| ("attach", schema)),
134 )
135 .map(|(request, schema)| {
136 json!({
137 "if": {
138 "properties": {
139 "request": {
140 "const": request
141 }
142 },
143 "required": ["request"]
144 },
145 "then": schema
146 })
147 })
148 .collect::<Vec<_>>();
149
150 let schema = json!({
151 "allOf": conjuncts
152 });
153 Ok(schema)
154 }
155}
156
157#[async_trait(?Send)]
158impl DebugAdapter for GoDebugAdapter {
159 fn name(&self) -> DebugAdapterName {
160 DebugAdapterName(Self::ADAPTER_NAME.into())
161 }
162
163 fn adapter_language_name(&self) -> Option<LanguageName> {
164 Some(SharedString::new_static("Go").into())
165 }
166
167 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
168 static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
169 const RAW_SCHEMA: &str = include_str!("../schemas/Delve.json");
170 serde_json::from_str(RAW_SCHEMA).unwrap()
171 });
172 Cow::Borrowed(&*SCHEMA)
173 }
174
175 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
176 let mut args = match &zed_scenario.request {
177 dap::DebugRequest::Attach(attach_config) => {
178 json!({
179 "request": "attach",
180 "mode": "debug",
181 "processId": attach_config.process_id,
182 })
183 }
184 dap::DebugRequest::Launch(launch_config) => {
185 let mode = if launch_config.program != "." {
186 "exec"
187 } else {
188 "debug"
189 };
190
191 json!({
192 "request": "launch",
193 "mode": mode,
194 "program": launch_config.program,
195 "cwd": launch_config.cwd,
196 "args": launch_config.args,
197 "env": launch_config.env_json()
198 })
199 }
200 };
201
202 let map = args.as_object_mut().unwrap();
203
204 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
205 map.insert("stopOnEntry".into(), stop_on_entry.into());
206 }
207
208 Ok(DebugScenario {
209 adapter: zed_scenario.adapter,
210 label: zed_scenario.label,
211 build: None,
212 config: args,
213 tcp_connection: None,
214 })
215 }
216
217 async fn get_binary(
218 &self,
219 delegate: &Arc<dyn DapDelegate>,
220 task_definition: &DebugTaskDefinition,
221 user_installed_path: Option<PathBuf>,
222 user_args: Option<Vec<String>>,
223 _cx: &mut AsyncApp,
224 ) -> Result<DebugAdapterBinary> {
225 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
226 let dlv_path = adapter_path.join("dlv");
227
228 let delve_path = if let Some(path) = user_installed_path {
229 path.to_string_lossy().to_string()
230 } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
231 path.to_string_lossy().to_string()
232 } else if delegate.fs().is_file(&dlv_path).await {
233 dlv_path.to_string_lossy().to_string()
234 } else {
235 let go = delegate
236 .which(OsStr::new("go"))
237 .await
238 .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
239
240 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
241
242 let install_output = util::command::new_smol_command(&go)
243 .env("GO111MODULE", "on")
244 .env("GOBIN", &adapter_path)
245 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
246 .output()
247 .await?;
248
249 if !install_output.status.success() {
250 bail!(
251 "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'",
252 String::from_utf8_lossy(&install_output.stdout),
253 String::from_utf8_lossy(&install_output.stderr)
254 );
255 }
256
257 adapter_path.join("dlv").to_string_lossy().to_string()
258 };
259
260 let cwd = Some(
261 task_definition
262 .config
263 .get("cwd")
264 .and_then(|s| s.as_str())
265 .map(PathBuf::from)
266 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
267 );
268
269 let arguments;
270 let command;
271 let connection;
272
273 let mut configuration = task_definition.config.clone();
274 let mut envs = HashMap::default();
275
276 if let Some(configuration) = configuration.as_object_mut() {
277 configuration
278 .entry("cwd")
279 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
280
281 handle_envs(
282 configuration,
283 &mut envs,
284 cwd.as_deref(),
285 delegate.fs().clone(),
286 )
287 .await;
288 }
289
290 if let Some(connection_options) = &task_definition.tcp_connection {
291 command = None;
292 arguments = vec![];
293 let (host, port, timeout) =
294 crate::configure_tcp_connection(connection_options.clone()).await?;
295 connection = Some(TcpArguments {
296 host,
297 port,
298 timeout,
299 });
300 } else {
301 let minidelve_path = self.install_shim(delegate).await?;
302 let (host, port, _) =
303 crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
304 command = Some(minidelve_path.to_string_lossy().into_owned());
305 connection = None;
306 arguments = if let Some(mut args) = user_args {
307 args.insert(0, delve_path);
308 args
309 } else if cfg!(windows) {
310 vec![
311 delve_path,
312 "dap".into(),
313 "--listen".into(),
314 format!("{}:{}", host, port),
315 "--headless".into(),
316 ]
317 } else {
318 vec![
319 delve_path,
320 "dap".into(),
321 "--listen".into(),
322 format!("{}:{}", host, port),
323 ]
324 };
325 }
326 Ok(DebugAdapterBinary {
327 command,
328 arguments,
329 cwd,
330 envs,
331 connection,
332 request_args: StartDebuggingRequestArguments {
333 configuration,
334 request: self.request_kind(&task_definition.config).await?,
335 },
336 })
337 }
338}
339
340// delve doesn't do anything with the envFile setting, so we intercept it
341async fn handle_envs(
342 config: &mut Map<String, Value>,
343 envs: &mut HashMap<String, String>,
344 cwd: Option<&Path>,
345 fs: Arc<dyn Fs>,
346) -> Option<()> {
347 let env_files = match config.get("envFile")? {
348 Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
349 Value::String(s) => vec![Some(s.as_str())],
350 _ => return None,
351 };
352
353 let rebase_path = |path: PathBuf| {
354 if path.is_absolute() {
355 Some(path)
356 } else {
357 cwd.map(|p| p.join(path))
358 }
359 };
360
361 for path in env_files {
362 let Some(path) = path
363 .and_then(|s| PathBuf::from_str(s).ok())
364 .and_then(rebase_path)
365 else {
366 continue;
367 };
368
369 if let Ok(file) = fs.open_sync(&path).await {
370 envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
371 } else {
372 warn!("While starting Go debug session: failed to read env file {path:?}");
373 };
374 }
375
376 // remove envFile now that it's been handled
377 config.remove("entry");
378 Some(())
379}