javascript.rs

  1use adapters::latest_github_release;
  2use anyhow::Context as _;
  3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
  4use fs::{Fs, RealFs};
  5use gpui::{AsyncApp, background_executor};
  6use serde_json::Value;
  7use std::{
  8    borrow::Cow,
  9    collections::HashMap,
 10    path::{Path, PathBuf},
 11    sync::{LazyLock, OnceLock},
 12};
 13use task::DebugRequest;
 14use util::{ResultExt, maybe};
 15
 16use crate::*;
 17
 18#[derive(Debug, Default)]
 19pub struct JsDebugAdapter {
 20    checked: OnceLock<()>,
 21}
 22
 23impl JsDebugAdapter {
 24    pub const ADAPTER_NAME: &'static str = "JavaScript";
 25    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
 26    const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
 27
 28    async fn fetch_latest_adapter_version(
 29        &self,
 30        delegate: &Arc<dyn DapDelegate>,
 31    ) -> Result<AdapterVersion> {
 32        let release = latest_github_release(
 33            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
 34            true,
 35            false,
 36            delegate.http_client(),
 37        )
 38        .await?;
 39
 40        let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
 41
 42        Ok(AdapterVersion {
 43            tag_name: release.tag_name,
 44            url: release
 45                .assets
 46                .iter()
 47                .find(|asset| asset.name == asset_name)
 48                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 49                .browser_download_url
 50                .clone(),
 51        })
 52    }
 53
 54    async fn get_installed_binary(
 55        &self,
 56        delegate: &Arc<dyn DapDelegate>,
 57        task_definition: &DebugTaskDefinition,
 58        user_installed_path: Option<PathBuf>,
 59        user_args: Option<Vec<String>>,
 60        _: &mut AsyncApp,
 61    ) -> Result<DebugAdapterBinary> {
 62        let adapter_path = if let Some(user_installed_path) = user_installed_path {
 63            user_installed_path
 64        } else {
 65            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 66
 67            let file_name_prefix = format!("{}_", self.name());
 68
 69            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 70                file_name.starts_with(&file_name_prefix)
 71            })
 72            .await
 73            .context("Couldn't find JavaScript dap directory")?
 74        };
 75
 76        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
 77        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 78
 79        let mut configuration = task_definition.config.clone();
 80        if let Some(configuration) = configuration.as_object_mut() {
 81            maybe!({
 82                configuration
 83                    .get("type")
 84                    .filter(|value| value == &"node-terminal")?;
 85                let command = configuration.get("command")?.as_str()?.to_owned();
 86                let mut args = shlex::split(&command)?.into_iter();
 87                let program = args.next()?;
 88                configuration.insert("runtimeExecutable".to_owned(), program.into());
 89                configuration.insert(
 90                    "runtimeArgs".to_owned(),
 91                    args.map(Value::from).collect::<Vec<_>>().into(),
 92                );
 93                configuration.insert("console".to_owned(), "externalTerminal".into());
 94                Some(())
 95            });
 96
 97            configuration.entry("type").and_modify(normalize_task_type);
 98
 99            if let Some(program) = configuration
100                .get("program")
101                .cloned()
102                .and_then(|value| value.as_str().map(str::to_owned))
103            {
104                match program.as_str() {
105                    "npm" | "pnpm" | "yarn" | "bun"
106                        if !configuration.contains_key("runtimeExecutable")
107                            && !configuration.contains_key("runtimeArgs") =>
108                    {
109                        configuration.remove("program");
110                        configuration.insert("runtimeExecutable".to_owned(), program.into());
111                        if let Some(args) = configuration.remove("args") {
112                            configuration.insert("runtimeArgs".to_owned(), args);
113                        }
114                    }
115                    _ => {}
116                }
117            }
118
119            configuration
120                .entry("cwd")
121                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
122
123            configuration
124                .entry("console")
125                .or_insert("externalTerminal".into());
126
127            configuration.entry("sourceMaps").or_insert(true.into());
128            configuration
129                .entry("pauseForSourceMap")
130                .or_insert(true.into());
131            configuration
132                .entry("sourceMapRenames")
133                .or_insert(true.into());
134        }
135
136        let arguments = if let Some(mut args) = user_args {
137            args.insert(
138                0,
139                adapter_path
140                    .join(Self::ADAPTER_PATH)
141                    .to_string_lossy()
142                    .to_string(),
143            );
144            args
145        } else {
146            vec![
147                adapter_path
148                    .join(Self::ADAPTER_PATH)
149                    .to_string_lossy()
150                    .to_string(),
151                port.to_string(),
152                host.to_string(),
153            ]
154        };
155
156        Ok(DebugAdapterBinary {
157            command: Some(
158                delegate
159                    .node_runtime()
160                    .binary_path()
161                    .await?
162                    .to_string_lossy()
163                    .into_owned(),
164            ),
165            arguments,
166            cwd: Some(delegate.worktree_root_path().to_path_buf()),
167            envs: HashMap::default(),
168            connection: Some(adapters::TcpArguments {
169                host,
170                port,
171                timeout,
172            }),
173            request_args: StartDebuggingRequestArguments {
174                configuration,
175                request: self.request_kind(&task_definition.config).await?,
176            },
177        })
178    }
179}
180
181#[async_trait(?Send)]
182impl DebugAdapter for JsDebugAdapter {
183    fn name(&self) -> DebugAdapterName {
184        DebugAdapterName(Self::ADAPTER_NAME.into())
185    }
186
187    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
188        let mut args = json!({
189            "type": "pwa-node",
190            "request": match zed_scenario.request {
191                DebugRequest::Launch(_) => "launch",
192                DebugRequest::Attach(_) => "attach",
193            },
194        });
195
196        let map = args.as_object_mut().unwrap();
197        match &zed_scenario.request {
198            DebugRequest::Attach(attach) => {
199                map.insert("processId".into(), attach.process_id.into());
200            }
201            DebugRequest::Launch(launch) => {
202                if launch.program.starts_with("http://") {
203                    map.insert("url".into(), launch.program.clone().into());
204                } else {
205                    map.insert("program".into(), launch.program.clone().into());
206                }
207
208                if !launch.args.is_empty() {
209                    map.insert("args".into(), launch.args.clone().into());
210                }
211                if !launch.env.is_empty() {
212                    map.insert("env".into(), launch.env_json());
213                }
214
215                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
216                    map.insert("stopOnEntry".into(), stop_on_entry.into());
217                }
218                if let Some(cwd) = launch.cwd.as_ref() {
219                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
220                }
221            }
222        };
223
224        Ok(DebugScenario {
225            adapter: zed_scenario.adapter,
226            label: zed_scenario.label,
227            build: None,
228            config: args,
229            tcp_connection: None,
230        })
231    }
232
233    fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
234        static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
235            const RAW_SCHEMA: &str = include_str!("../schemas/JavaScript.json");
236            serde_json::from_str(RAW_SCHEMA).unwrap()
237        });
238        Cow::Borrowed(&*SCHEMA)
239    }
240
241    async fn get_binary(
242        &self,
243        delegate: &Arc<dyn DapDelegate>,
244        config: &DebugTaskDefinition,
245        user_installed_path: Option<PathBuf>,
246        user_args: Option<Vec<String>>,
247        cx: &mut AsyncApp,
248    ) -> Result<DebugAdapterBinary> {
249        if self.checked.set(()).is_ok() {
250            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
251            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
252                adapters::download_adapter_from_github(
253                    self.name(),
254                    version,
255                    adapters::DownloadedFileType::GzipTar,
256                    paths::debug_adapters_dir(),
257                    delegate.as_ref(),
258                )
259                .await?;
260            } else {
261                delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
262            }
263        }
264
265        self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
266            .await
267    }
268
269    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
270        let label = args
271            .configuration
272            .get("name")?
273            .as_str()
274            .filter(|name| !name.is_empty())?;
275        Some(label.to_owned())
276    }
277}
278
279impl JsDebugAdapter {
280    pub fn fetch_schema(dir: &Path) -> anyhow::Result<(String, String)> {
281        let executor = background_executor();
282        // FIXME
283        let client = Arc::new(reqwest_client::ReqwestClient::user_agent("Cole").unwrap());
284        let fs = Arc::new(RealFs::new(None, executor.clone()));
285        let delegate = UpdateSchemasDapDelegate {
286            client: client.clone(),
287            fs: fs.clone(),
288        };
289
290        executor.block(async move {
291            let release = latest_github_release(
292                &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
293                true,
294                false,
295                client.clone(),
296            )
297            .await?;
298
299            let version = release.tag_name.strip_prefix("v").unwrap();
300            let asset_name = format!("ms-vscode.js-debug.{version}.vsix",);
301            let version = AdapterVersion {
302                tag_name: release.tag_name,
303                url: release
304                    .assets
305                    .iter()
306                    .find(|asset| asset.name == asset_name)
307                    .with_context(|| format!("no asset found matching {asset_name:?}"))?
308                    .browser_download_url
309                    .clone(),
310            };
311
312            let path = adapters::download_adapter_from_github(
313                DebugAdapterName(Self::ADAPTER_NAME.into()),
314                version,
315                adapters::DownloadedFileType::Vsix,
316                dir,
317                &delegate,
318            )
319            .await?;
320            let package_json_content = fs
321                .load(&path.join("extension").join("package.json"))
322                .await?;
323            let package_nls_json_content = fs
324                .load(&path.join("extension").join("package.nls.json"))
325                .await?;
326            Ok((package_json_content, package_nls_json_content))
327        })
328    }
329}
330
331fn normalize_task_type(task_type: &mut Value) {
332    let Some(task_type_str) = task_type.as_str() else {
333        return;
334    };
335
336    let new_name = match task_type_str {
337        "node" | "pwa-node" | "node-terminal" => "pwa-node",
338        "chrome" | "pwa-chrome" => "pwa-chrome",
339        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
340        _ => task_type_str,
341    }
342    .to_owned();
343
344    *task_type = Value::String(new_name);
345}