javascript.rs

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