go.rs

  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}