javascript.rs

  1use adapters::latest_github_release;
  2use anyhow::Context as _;
  3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
  4use gpui::AsyncApp;
  5use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
  6use task::DebugRequest;
  7use util::ResultExt;
  8
  9use crate::*;
 10
 11#[derive(Debug, Default)]
 12pub(crate) struct JsDebugAdapter {
 13    checked: OnceLock<()>,
 14}
 15
 16impl JsDebugAdapter {
 17    const ADAPTER_NAME: &'static str = "JavaScript";
 18    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
 19    const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
 20
 21    fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
 22        let mut args = json!({
 23            "type": "pwa-node",
 24            "request": match config.request {
 25                DebugRequest::Launch(_) => "launch",
 26                DebugRequest::Attach(_) => "attach",
 27            },
 28        });
 29        let map = args.as_object_mut().unwrap();
 30        match &config.request {
 31            DebugRequest::Attach(attach) => {
 32                map.insert("processId".into(), attach.process_id.into());
 33            }
 34            DebugRequest::Launch(launch) => {
 35                map.insert("program".into(), launch.program.clone().into());
 36
 37                if !launch.args.is_empty() {
 38                    map.insert("args".into(), launch.args.clone().into());
 39                }
 40                if !launch.env.is_empty() {
 41                    map.insert("env".into(), launch.env_json());
 42                }
 43
 44                if let Some(stop_on_entry) = config.stop_on_entry {
 45                    map.insert("stopOnEntry".into(), stop_on_entry.into());
 46                }
 47                if let Some(cwd) = launch.cwd.as_ref() {
 48                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
 49                }
 50            }
 51        }
 52        StartDebuggingRequestArguments {
 53            configuration: args,
 54            request: config.request.to_dap(),
 55        }
 56    }
 57
 58    async fn fetch_latest_adapter_version(
 59        &self,
 60        delegate: &Arc<dyn DapDelegate>,
 61    ) -> Result<AdapterVersion> {
 62        let release = latest_github_release(
 63            &format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
 64            true,
 65            false,
 66            delegate.http_client(),
 67        )
 68        .await?;
 69
 70        let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
 71
 72        Ok(AdapterVersion {
 73            tag_name: release.tag_name,
 74            url: release
 75                .assets
 76                .iter()
 77                .find(|asset| asset.name == asset_name)
 78                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 79                .browser_download_url
 80                .clone(),
 81        })
 82    }
 83
 84    async fn get_installed_binary(
 85        &self,
 86        delegate: &Arc<dyn DapDelegate>,
 87        config: &DebugTaskDefinition,
 88        user_installed_path: Option<PathBuf>,
 89        _: &mut AsyncApp,
 90    ) -> Result<DebugAdapterBinary> {
 91        let adapter_path = if let Some(user_installed_path) = user_installed_path {
 92            user_installed_path
 93        } else {
 94            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 95
 96            let file_name_prefix = format!("{}_", self.name());
 97
 98            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 99                file_name.starts_with(&file_name_prefix)
100            })
101            .await
102            .context("Couldn't find JavaScript dap directory")?
103        };
104
105        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
106        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
107
108        Ok(DebugAdapterBinary {
109            command: delegate
110                .node_runtime()
111                .binary_path()
112                .await?
113                .to_string_lossy()
114                .into_owned(),
115            arguments: vec![
116                adapter_path
117                    .join(Self::ADAPTER_PATH)
118                    .to_string_lossy()
119                    .to_string(),
120                port.to_string(),
121                host.to_string(),
122            ],
123            cwd: None,
124            envs: HashMap::default(),
125            connection: Some(adapters::TcpArguments {
126                host,
127                port,
128                timeout,
129            }),
130            request_args: self.request_args(config),
131        })
132    }
133}
134
135#[async_trait(?Send)]
136impl DebugAdapter for JsDebugAdapter {
137    fn name(&self) -> DebugAdapterName {
138        DebugAdapterName(Self::ADAPTER_NAME.into())
139    }
140
141    async fn get_binary(
142        &self,
143        delegate: &Arc<dyn DapDelegate>,
144        config: &DebugTaskDefinition,
145        user_installed_path: Option<PathBuf>,
146        cx: &mut AsyncApp,
147    ) -> Result<DebugAdapterBinary> {
148        if self.checked.set(()).is_ok() {
149            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
150            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
151                adapters::download_adapter_from_github(
152                    self.name(),
153                    version,
154                    adapters::DownloadedFileType::GzipTar,
155                    delegate.as_ref(),
156                )
157                .await?;
158            }
159        }
160
161        self.get_installed_binary(delegate, &config, user_installed_path, cx)
162            .await
163    }
164}