javascript.rs

  1use adapters::latest_github_release;
  2use anyhow::Context as _;
  3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
  4use fs::Fs;
  5use gpui::{AsyncApp, BackgroundExecutor};
  6use serde::{Deserialize, Serialize};
  7use serde_json::Value;
  8use std::{
  9    borrow::Cow,
 10    collections::HashMap,
 11    path::{Path, PathBuf},
 12    sync::{LazyLock, OnceLock},
 13};
 14use task::{DebugRequest, EnvVariableReplacer, VariableName};
 15use tempfile::TempDir;
 16use util::{ResultExt, maybe};
 17
 18use crate::*;
 19
 20#[derive(Debug, Default)]
 21pub struct JsDebugAdapter {
 22    checked: OnceLock<()>,
 23}
 24
 25impl JsDebugAdapter {
 26    pub const ADAPTER_NAME: &'static str = "JavaScript";
 27    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
 28    const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
 29
 30    async fn fetch_latest_adapter_version(
 31        &self,
 32        delegate: &Arc<dyn DapDelegate>,
 33    ) -> Result<AdapterVersion> {
 34        let release = latest_github_release(
 35            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
 36            true,
 37            false,
 38            delegate.http_client(),
 39        )
 40        .await?;
 41
 42        let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
 43
 44        Ok(AdapterVersion {
 45            tag_name: release.tag_name,
 46            url: release
 47                .assets
 48                .iter()
 49                .find(|asset| asset.name == asset_name)
 50                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 51                .browser_download_url
 52                .clone(),
 53        })
 54    }
 55
 56    async fn get_installed_binary(
 57        &self,
 58        delegate: &Arc<dyn DapDelegate>,
 59        task_definition: &DebugTaskDefinition,
 60        user_installed_path: Option<PathBuf>,
 61        user_args: Option<Vec<String>>,
 62        _: &mut AsyncApp,
 63    ) -> Result<DebugAdapterBinary> {
 64        let adapter_path = if let Some(user_installed_path) = user_installed_path {
 65            user_installed_path
 66        } else {
 67            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 68
 69            let file_name_prefix = format!("{}_", self.name());
 70
 71            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 72                file_name.starts_with(&file_name_prefix)
 73            })
 74            .await
 75            .context("Couldn't find JavaScript dap directory")?
 76        };
 77
 78        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
 79        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 80
 81        let mut configuration = task_definition.config.clone();
 82        if let Some(configuration) = configuration.as_object_mut() {
 83            maybe!({
 84                configuration
 85                    .get("type")
 86                    .filter(|value| value == &"node-terminal")?;
 87                let command = configuration.get("command")?.as_str()?.to_owned();
 88                let mut args = shlex::split(&command)?.into_iter();
 89                let program = args.next()?;
 90                configuration.insert("runtimeExecutable".to_owned(), program.into());
 91                configuration.insert(
 92                    "runtimeArgs".to_owned(),
 93                    args.map(Value::from).collect::<Vec<_>>().into(),
 94                );
 95                configuration.insert("console".to_owned(), "externalTerminal".into());
 96                Some(())
 97            });
 98
 99            configuration.entry("type").and_modify(normalize_task_type);
100
101            if let Some(program) = configuration
102                .get("program")
103                .cloned()
104                .and_then(|value| value.as_str().map(str::to_owned))
105            {
106                match program.as_str() {
107                    "npm" | "pnpm" | "yarn" | "bun"
108                        if !configuration.contains_key("runtimeExecutable")
109                            && !configuration.contains_key("runtimeArgs") =>
110                    {
111                        configuration.remove("program");
112                        configuration.insert("runtimeExecutable".to_owned(), program.into());
113                        if let Some(args) = configuration.remove("args") {
114                            configuration.insert("runtimeArgs".to_owned(), args);
115                        }
116                    }
117                    _ => {}
118                }
119            }
120
121            configuration
122                .entry("cwd")
123                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
124
125            configuration
126                .entry("console")
127                .or_insert("externalTerminal".into());
128
129            configuration.entry("sourceMaps").or_insert(true.into());
130            configuration
131                .entry("pauseForSourceMap")
132                .or_insert(true.into());
133            configuration
134                .entry("sourceMapRenames")
135                .or_insert(true.into());
136        }
137
138        let arguments = if let Some(mut args) = user_args {
139            args.insert(
140                0,
141                adapter_path
142                    .join(Self::ADAPTER_PATH)
143                    .to_string_lossy()
144                    .to_string(),
145            );
146            args
147        } else {
148            vec![
149                adapter_path
150                    .join(Self::ADAPTER_PATH)
151                    .to_string_lossy()
152                    .to_string(),
153                port.to_string(),
154                host.to_string(),
155            ]
156        };
157
158        Ok(DebugAdapterBinary {
159            command: Some(
160                delegate
161                    .node_runtime()
162                    .binary_path()
163                    .await?
164                    .to_string_lossy()
165                    .into_owned(),
166            ),
167            arguments,
168            cwd: Some(delegate.worktree_root_path().to_path_buf()),
169            envs: HashMap::default(),
170            connection: Some(adapters::TcpArguments {
171                host,
172                port,
173                timeout,
174            }),
175            request_args: StartDebuggingRequestArguments {
176                configuration,
177                request: self.request_kind(&task_definition.config).await?,
178            },
179        })
180    }
181}
182
183#[async_trait(?Send)]
184impl DebugAdapter for JsDebugAdapter {
185    fn name(&self) -> DebugAdapterName {
186        DebugAdapterName(Self::ADAPTER_NAME.into())
187    }
188
189    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
190        let mut args = json!({
191            "type": "pwa-node",
192            "request": match zed_scenario.request {
193                DebugRequest::Launch(_) => "launch",
194                DebugRequest::Attach(_) => "attach",
195            },
196        });
197
198        let map = args.as_object_mut().unwrap();
199        match &zed_scenario.request {
200            DebugRequest::Attach(attach) => {
201                map.insert("processId".into(), attach.process_id.into());
202            }
203            DebugRequest::Launch(launch) => {
204                if launch.program.starts_with("http://") {
205                    map.insert("url".into(), launch.program.clone().into());
206                } else {
207                    map.insert("program".into(), launch.program.clone().into());
208                }
209
210                if !launch.args.is_empty() {
211                    map.insert("args".into(), launch.args.clone().into());
212                }
213                if !launch.env.is_empty() {
214                    map.insert("env".into(), launch.env_json());
215                }
216
217                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
218                    map.insert("stopOnEntry".into(), stop_on_entry.into());
219                }
220                if let Some(cwd) = launch.cwd.as_ref() {
221                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
222                }
223            }
224        };
225
226        Ok(DebugScenario {
227            adapter: zed_scenario.adapter,
228            label: zed_scenario.label,
229            build: None,
230            config: args,
231            tcp_connection: None,
232        })
233    }
234
235    fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
236        static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
237            const RAW_SCHEMA: &str = include_str!("../schemas/JavaScript.json");
238            serde_json::from_str(RAW_SCHEMA).unwrap()
239        });
240        Cow::Borrowed(&*SCHEMA)
241    }
242
243    async fn get_binary(
244        &self,
245        delegate: &Arc<dyn DapDelegate>,
246        config: &DebugTaskDefinition,
247        user_installed_path: Option<PathBuf>,
248        user_args: Option<Vec<String>>,
249        cx: &mut AsyncApp,
250    ) -> Result<DebugAdapterBinary> {
251        if self.checked.set(()).is_ok() {
252            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
253            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
254                adapters::download_adapter_from_github(
255                    self.name(),
256                    version,
257                    adapters::DownloadedFileType::GzipTar,
258                    paths::debug_adapters_dir(),
259                    delegate.as_ref(),
260                )
261                .await?;
262            } else {
263                delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
264            }
265        }
266
267        self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
268            .await
269    }
270
271    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
272        let label = args
273            .configuration
274            .get("name")?
275            .as_str()
276            .filter(|name| !name.is_empty())?;
277        Some(label.to_owned())
278    }
279}
280
281#[cfg(feature = "update-schemas")]
282impl JsDebugAdapter {
283    pub fn get_schema(
284        temp_dir: &TempDir,
285        output_dir: &Path,
286        executor: BackgroundExecutor,
287    ) -> anyhow::Result<()> {
288        #[derive(Serialize, Deserialize)]
289        struct PackageJsonConfigurationAttributes {
290            #[serde(default, skip_serializing_if = "Option::is_none")]
291            launch: Option<serde_json::Value>,
292            #[serde(default, skip_serializing_if = "Option::is_none")]
293            attach: Option<serde_json::Value>,
294        }
295
296        #[derive(Serialize, Deserialize)]
297        #[serde(rename_all = "camelCase")]
298        struct PackageJsonDebugger {
299            r#type: String,
300            configuration_attributes: PackageJsonConfigurationAttributes,
301        }
302
303        #[derive(Serialize, Deserialize)]
304        struct PackageJsonContributes {
305            debuggers: Vec<PackageJsonDebugger>,
306        }
307
308        #[derive(Serialize, Deserialize)]
309        struct PackageJson {
310            contributes: PackageJsonContributes,
311        }
312
313        let temp_dir = std::fs::canonicalize(temp_dir.path())?;
314        let delegate = UpdateSchemasDapDelegate::new(executor.clone());
315        let fs = delegate.fs.clone();
316        let client = delegate.client.clone();
317
318        let (package_json, package_nls_json) = executor.block(async move {
319            let release = latest_github_release(
320                &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
321                true,
322                false,
323                client.clone(),
324            )
325            .await?;
326
327            let version = release
328                .tag_name
329                .strip_prefix("v")
330                .context("parse version")?;
331            let asset_name = format!("ms-vscode.js-debug.{version}.vsix",);
332            let version = AdapterVersion {
333                tag_name: release.tag_name,
334                url: release
335                    .assets
336                    .iter()
337                    .find(|asset| asset.name == asset_name)
338                    .with_context(|| format!("no asset found matching {asset_name:?}"))?
339                    .browser_download_url
340                    .clone(),
341            };
342
343            let path = adapters::download_adapter_from_github(
344                DebugAdapterName(Self::ADAPTER_NAME.into()),
345                version,
346                adapters::DownloadedFileType::Vsix,
347                &temp_dir,
348                &delegate,
349            )
350            .await?;
351            let package_json = fs
352                .load(&path.join("extension").join("package.json"))
353                .await?;
354            let package_nls_json = fs
355                .load(&path.join("extension").join("package.nls.json"))
356                .await?;
357            anyhow::Ok((package_json, package_nls_json))
358        })?;
359
360        let package_nls_json =
361            serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_nls_json)?
362                .into_iter()
363                .filter_map(|(k, v)| {
364                    let v = v.as_str()?;
365                    Some((k, v.to_owned()))
366                })
367                .collect();
368
369        let package_json: serde_json::Value = serde_json::from_str(&package_json)?;
370
371        struct Replacer {
372            package_nls_json: HashMap<String, String>,
373            env: EnvVariableReplacer,
374        }
375
376        impl Replacer {
377            fn replace(&self, input: serde_json::Value) -> serde_json::Value {
378                match input {
379                    serde_json::Value::String(s) => {
380                        if s.starts_with("%") && s.ends_with("%") {
381                            self.package_nls_json
382                                .get(s.trim_matches('%'))
383                                .map(|s| s.as_str().into())
384                                .unwrap_or("(missing)".into())
385                        } else {
386                            self.env.replace(&s).into()
387                        }
388                    }
389                    serde_json::Value::Array(arr) => {
390                        serde_json::Value::Array(arr.into_iter().map(|v| self.replace(v)).collect())
391                    }
392                    serde_json::Value::Object(obj) => serde_json::Value::Object(
393                        obj.into_iter().map(|(k, v)| (k, self.replace(v))).collect(),
394                    ),
395                    _ => input,
396                }
397            }
398        }
399
400        let env = EnvVariableReplacer::new(HashMap::from_iter([(
401            "workspaceFolder".to_owned(),
402            VariableName::WorktreeRoot.to_string(),
403        )]));
404        let replacer = Replacer {
405            env,
406            package_nls_json,
407        };
408        let package_json = replacer.replace(package_json);
409
410        let package_json: PackageJson = serde_json::from_value(package_json)?;
411
412        let types = package_json
413            .contributes
414            .debuggers
415            .iter()
416            .map(|debugger| debugger.r#type.clone())
417            .collect::<Vec<_>>();
418        let mut conjuncts = package_json
419            .contributes
420            .debuggers
421            .into_iter()
422            .flat_map(|debugger| {
423                let r#type = debugger.r#type;
424                let configuration_attributes = debugger.configuration_attributes;
425                configuration_attributes
426                    .launch
427                    .map(|schema| ("launch", schema))
428                    .into_iter()
429                    .chain(
430                        configuration_attributes
431                            .attach
432                            .map(|schema| ("attach", schema)),
433                    )
434                    .map(|(request, schema)| {
435                        json!({
436                            "if": {
437                                "properties": {
438                                    "type": {
439                                        "const": r#type
440                                    },
441                                    "request": {
442                                        "const": request
443                                    }
444                                },
445                                "required": ["type", "request"]
446                            },
447                            "then": schema
448                        })
449                    })
450                    .collect::<Vec<_>>()
451            })
452            .collect::<Vec<_>>();
453        conjuncts.push(json!({
454            "properties": {
455                "type": {
456                    "enum": types
457                }
458            },
459            "required": ["type"]
460        }));
461        let schema = json!({
462            "allOf": conjuncts
463        });
464
465        // FIXME figure out what to do about formatting
466        let mut schema = serde_json::to_string_pretty(&schema)?;
467        schema.push('\n');
468        std::fs::write(
469            output_dir.join(Self::ADAPTER_NAME).with_extension("json"),
470            schema,
471        )?;
472        Ok(())
473    }
474}
475
476fn normalize_task_type(task_type: &mut Value) {
477    let Some(task_type_str) = task_type.as_str() else {
478        return;
479    };
480
481    let new_name = match task_type_str {
482        "node" | "pwa-node" | "node-terminal" => "pwa-node",
483        "chrome" | "pwa-chrome" => "pwa-chrome",
484        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
485        _ => task_type_str,
486    }
487    .to_owned();
488
489    *task_type = Value::String(new_name);
490}