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        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}