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::{collections::HashMap, path::PathBuf, sync::OnceLock};
  7use task::DebugRequest;
  8use util::{ResultExt, maybe};
  9
 10use crate::*;
 11
 12#[derive(Debug, Default)]
 13pub(crate) struct JsDebugAdapter {
 14    checked: OnceLock<()>,
 15}
 16
 17impl JsDebugAdapter {
 18    const ADAPTER_NAME: &'static str = "JavaScript";
 19    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
 20    const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
 21
 22    async fn fetch_latest_adapter_version(
 23        &self,
 24        delegate: &Arc<dyn DapDelegate>,
 25    ) -> Result<AdapterVersion> {
 26        let release = latest_github_release(
 27            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
 28            true,
 29            false,
 30            delegate.http_client(),
 31        )
 32        .await?;
 33
 34        let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
 35
 36        Ok(AdapterVersion {
 37            tag_name: release.tag_name,
 38            url: release
 39                .assets
 40                .iter()
 41                .find(|asset| asset.name == asset_name)
 42                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 43                .browser_download_url
 44                .clone(),
 45        })
 46    }
 47
 48    async fn get_installed_binary(
 49        &self,
 50        delegate: &Arc<dyn DapDelegate>,
 51        task_definition: &DebugTaskDefinition,
 52        user_installed_path: Option<PathBuf>,
 53        user_args: Option<Vec<String>>,
 54        _: &mut AsyncApp,
 55    ) -> Result<DebugAdapterBinary> {
 56        let adapter_path = if let Some(user_installed_path) = user_installed_path {
 57            user_installed_path
 58        } else {
 59            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 60
 61            let file_name_prefix = format!("{}_", self.name());
 62
 63            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 64                file_name.starts_with(&file_name_prefix)
 65            })
 66            .await
 67            .context("Couldn't find JavaScript dap directory")?
 68        };
 69
 70        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
 71        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 72
 73        let mut configuration = task_definition.config.clone();
 74        if let Some(configuration) = configuration.as_object_mut() {
 75            maybe!({
 76                configuration
 77                    .get("type")
 78                    .filter(|value| value == &"node-terminal")?;
 79                let command = configuration.get("command")?.as_str()?.to_owned();
 80                let mut args = shlex::split(&command)?.into_iter();
 81                let program = args.next()?;
 82                configuration.insert("program".to_owned(), program.into());
 83                configuration.insert(
 84                    "args".to_owned(),
 85                    args.map(Value::from).collect::<Vec<_>>().into(),
 86                );
 87                configuration.insert("console".to_owned(), "externalTerminal".into());
 88                Some(())
 89            });
 90
 91            configuration.entry("type").and_modify(normalize_task_type);
 92
 93            if let Some(program) = configuration
 94                .get("program")
 95                .cloned()
 96                .and_then(|value| value.as_str().map(str::to_owned))
 97            {
 98                match program.as_str() {
 99                    "npm" | "pnpm" | "yarn" | "bun"
100                        if !configuration.contains_key("runtimeExecutable")
101                            && !configuration.contains_key("runtimeArgs") =>
102                    {
103                        configuration.remove("program");
104                        configuration.insert("runtimeExecutable".to_owned(), program.into());
105                        if let Some(args) = configuration.remove("args") {
106                            configuration.insert("runtimeArgs".to_owned(), args);
107                        }
108                    }
109                    _ => {}
110                }
111            }
112
113            configuration
114                .entry("cwd")
115                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
116
117            configuration
118                .entry("console")
119                .or_insert("externalTerminal".into());
120
121            configuration.entry("sourceMaps").or_insert(true.into());
122            configuration
123                .entry("pauseForSourceMap")
124                .or_insert(true.into());
125            configuration
126                .entry("sourceMapRenames")
127                .or_insert(true.into());
128        }
129
130        let arguments = if let Some(mut args) = user_args {
131            args.insert(
132                0,
133                adapter_path
134                    .join(Self::ADAPTER_PATH)
135                    .to_string_lossy()
136                    .to_string(),
137            );
138            args
139        } else {
140            vec![
141                adapter_path
142                    .join(Self::ADAPTER_PATH)
143                    .to_string_lossy()
144                    .to_string(),
145                port.to_string(),
146                host.to_string(),
147            ]
148        };
149
150        Ok(DebugAdapterBinary {
151            command: Some(
152                delegate
153                    .node_runtime()
154                    .binary_path()
155                    .await?
156                    .to_string_lossy()
157                    .into_owned(),
158            ),
159            arguments,
160            cwd: Some(delegate.worktree_root_path().to_path_buf()),
161            envs: HashMap::default(),
162            connection: Some(adapters::TcpArguments {
163                host,
164                port,
165                timeout,
166            }),
167            request_args: StartDebuggingRequestArguments {
168                configuration,
169                request: self.request_kind(&task_definition.config).await?,
170            },
171        })
172    }
173}
174
175#[async_trait(?Send)]
176impl DebugAdapter for JsDebugAdapter {
177    fn name(&self) -> DebugAdapterName {
178        DebugAdapterName(Self::ADAPTER_NAME.into())
179    }
180
181    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
182        let mut args = json!({
183            "type": "pwa-node",
184            "request": match zed_scenario.request {
185                DebugRequest::Launch(_) => "launch",
186                DebugRequest::Attach(_) => "attach",
187            },
188        });
189
190        let map = args.as_object_mut().unwrap();
191        match &zed_scenario.request {
192            DebugRequest::Attach(attach) => {
193                map.insert("processId".into(), attach.process_id.into());
194            }
195            DebugRequest::Launch(launch) => {
196                if launch.program.starts_with("http://") {
197                    map.insert("url".into(), launch.program.clone().into());
198                } else {
199                    map.insert("program".into(), launch.program.clone().into());
200                }
201
202                if !launch.args.is_empty() {
203                    map.insert("args".into(), launch.args.clone().into());
204                }
205                if !launch.env.is_empty() {
206                    map.insert("env".into(), launch.env_json());
207                }
208
209                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
210                    map.insert("stopOnEntry".into(), stop_on_entry.into());
211                }
212                if let Some(cwd) = launch.cwd.as_ref() {
213                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
214                }
215            }
216        };
217
218        Ok(DebugScenario {
219            adapter: zed_scenario.adapter,
220            label: zed_scenario.label,
221            build: None,
222            config: args,
223            tcp_connection: None,
224        })
225    }
226
227    fn dap_schema(&self) -> serde_json::Value {
228        json!({
229            "oneOf": [
230                {
231                    "allOf": [
232                        {
233                            "type": "object",
234                            "required": ["request"],
235                            "properties": {
236                                "request": {
237                                    "type": "string",
238                                    "enum": ["launch"],
239                                    "description": "Request to launch a new process"
240                                }
241                            }
242                        },
243                        {
244                            "type": "object",
245                            "properties": {
246                                "type": {
247                                    "type": "string",
248                                    "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
249                                    "description": "The type of debug session",
250                                    "default": "pwa-node"
251                                },
252                                "program": {
253                                    "type": "string",
254                                    "description": "Path to the program or file to debug"
255                                },
256                                "cwd": {
257                                    "type": "string",
258                                    "description": "Absolute path to the working directory of the program being debugged"
259                                },
260                                "args": {
261                                    "type": ["array", "string"],
262                                    "description": "Command line arguments passed to the program",
263                                    "items": {
264                                        "type": "string"
265                                    },
266                                    "default": []
267                                },
268                                "env": {
269                                    "type": "object",
270                                    "description": "Environment variables passed to the program",
271                                    "default": {}
272                                },
273                                "envFile": {
274                                    "type": ["string", "array"],
275                                    "description": "Path to a file containing environment variable definitions",
276                                    "items": {
277                                        "type": "string"
278                                    }
279                                },
280                                "stopOnEntry": {
281                                    "type": "boolean",
282                                    "description": "Automatically stop program after launch",
283                                    "default": false
284                                },
285                                "runtimeExecutable": {
286                                    "type": ["string", "null"],
287                                    "description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
288                                    "default": "node"
289                                },
290                                "runtimeArgs": {
291                                    "type": ["array", "null"],
292                                    "description": "Arguments passed to the runtime executable",
293                                    "items": {
294                                        "type": "string"
295                                    },
296                                    "default": []
297                                },
298                                "outFiles": {
299                                    "type": "array",
300                                    "description": "Glob patterns for locating generated JavaScript files",
301                                    "items": {
302                                        "type": "string"
303                                    },
304                                    "default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
305                                },
306                                "sourceMaps": {
307                                    "type": "boolean",
308                                    "description": "Use JavaScript source maps if they exist",
309                                    "default": true
310                                },
311                                "pauseForSourceMap": {
312                                    "type": "boolean",
313                                    "description": "Wait for source maps to load before setting breakpoints.",
314                                    "default": true
315                                },
316                                "sourceMapRenames": {
317                                    "type": "boolean",
318                                    "description": "Whether to use the \"names\" mapping in sourcemaps.",
319                                    "default": true
320                                },
321                                "sourceMapPathOverrides": {
322                                    "type": "object",
323                                    "description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
324                                    "default": {}
325                                },
326                                "restart": {
327                                    "type": ["boolean", "object"],
328                                    "description": "Restart session after Node.js has terminated",
329                                    "default": false
330                                },
331                                "trace": {
332                                    "type": ["boolean", "object"],
333                                    "description": "Enables logging of the Debug Adapter",
334                                    "default": false
335                                },
336                                "console": {
337                                    "type": "string",
338                                    "enum": ["internalConsole", "integratedTerminal"],
339                                    "description": "Where to launch the debug target",
340                                    "default": "internalConsole"
341                                },
342                                // Browser-specific
343                                "url": {
344                                    "type": ["string", "null"],
345                                    "description": "Will navigate to this URL and attach to it (browser debugging)"
346                                },
347                                "webRoot": {
348                                    "type": "string",
349                                    "description": "Workspace absolute path to the webserver root",
350                                    "default": "${ZED_WORKTREE_ROOT}"
351                                },
352                                "userDataDir": {
353                                    "type": ["string", "boolean"],
354                                    "description": "Path to a custom Chrome user profile (browser debugging)",
355                                    "default": true
356                                },
357                                "skipFiles": {
358                                    "type": "array",
359                                    "description": "An array of glob patterns for files to skip when debugging",
360                                    "items": {
361                                        "type": "string"
362                                    },
363                                    "default": ["<node_internals>/**"]
364                                },
365                                "timeout": {
366                                    "type": "number",
367                                    "description": "Retry for this number of milliseconds to connect to the debug adapter",
368                                    "default": 10000
369                                },
370                                "resolveSourceMapLocations": {
371                                    "type": ["array", "null"],
372                                    "description": "A list of minimatch patterns for source map resolution",
373                                    "items": {
374                                        "type": "string"
375                                    }
376                                }
377                            },
378                            "oneOf": [
379                                { "required": ["program"] },
380                                { "required": ["url"] }
381                            ]
382                        }
383                    ]
384                },
385                {
386                    "allOf": [
387                        {
388                            "type": "object",
389                            "required": ["request"],
390                            "properties": {
391                                "request": {
392                                    "type": "string",
393                                    "enum": ["attach"],
394                                    "description": "Request to attach to an existing process"
395                                }
396                            }
397                        },
398                        {
399                            "type": "object",
400                            "properties": {
401                                "type": {
402                                    "type": "string",
403                                    "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
404                                    "description": "The type of debug session",
405                                    "default": "pwa-node"
406                                },
407                                "processId": {
408                                    "type": ["string", "number"],
409                                    "description": "ID of process to attach to (Node.js debugging)"
410                                },
411                                "port": {
412                                    "type": "number",
413                                    "description": "Debug port to attach to",
414                                    "default": 9229
415                                },
416                                "address": {
417                                    "type": "string",
418                                    "description": "TCP/IP address of the process to be debugged",
419                                    "default": "localhost"
420                                },
421                                "restart": {
422                                    "type": ["boolean", "object"],
423                                    "description": "Restart session after Node.js has terminated",
424                                    "default": false
425                                },
426                                "sourceMaps": {
427                                    "type": "boolean",
428                                    "description": "Use JavaScript source maps if they exist",
429                                    "default": true
430                                },
431                                "sourceMapPathOverrides": {
432                                    "type": "object",
433                                    "description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
434                                    "default": {}
435                                },
436                                "outFiles": {
437                                    "type": "array",
438                                    "description": "Glob patterns for locating generated JavaScript files",
439                                    "items": {
440                                        "type": "string"
441                                    },
442                                    "default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
443                                },
444                                "url": {
445                                    "type": "string",
446                                    "description": "Will search for a page with this URL and attach to it (browser debugging)"
447                                },
448                                "webRoot": {
449                                    "type": "string",
450                                    "description": "Workspace absolute path to the webserver root",
451                                    "default": "${ZED_WORKTREE_ROOT}"
452                                },
453                                "skipFiles": {
454                                    "type": "array",
455                                    "description": "An array of glob patterns for files to skip when debugging",
456                                    "items": {
457                                        "type": "string"
458                                    },
459                                    "default": ["<node_internals>/**"]
460                                },
461                                "timeout": {
462                                    "type": "number",
463                                    "description": "Retry for this number of milliseconds to connect to the debug adapter",
464                                    "default": 10000
465                                },
466                                "resolveSourceMapLocations": {
467                                    "type": ["array", "null"],
468                                    "description": "A list of minimatch patterns for source map resolution",
469                                    "items": {
470                                        "type": "string"
471                                    }
472                                },
473                                "remoteRoot": {
474                                    "type": ["string", "null"],
475                                    "description": "Path to the remote directory containing the program"
476                                },
477                                "localRoot": {
478                                    "type": ["string", "null"],
479                                    "description": "Path to the local directory containing the program"
480                                }
481                            },
482                            "oneOf": [
483                                { "required": ["processId"] },
484                                { "required": ["port"] }
485                            ]
486                        }
487                    ]
488                }
489            ]
490        })
491    }
492
493    async fn get_binary(
494        &self,
495        delegate: &Arc<dyn DapDelegate>,
496        config: &DebugTaskDefinition,
497        user_installed_path: Option<PathBuf>,
498        user_args: Option<Vec<String>>,
499        cx: &mut AsyncApp,
500    ) -> Result<DebugAdapterBinary> {
501        if self.checked.set(()).is_ok() {
502            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
503            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
504                adapters::download_adapter_from_github(
505                    self.name(),
506                    version,
507                    adapters::DownloadedFileType::GzipTar,
508                    delegate.as_ref(),
509                )
510                .await?;
511            } else {
512                delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
513            }
514        }
515
516        self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
517            .await
518    }
519
520    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
521        let label = args.configuration.get("name")?.as_str()?;
522        Some(label.to_owned())
523    }
524}
525
526fn normalize_task_type(task_type: &mut Value) {
527    let Some(task_type_str) = task_type.as_str() else {
528        return;
529    };
530
531    let new_name = match task_type_str {
532        "node" | "pwa-node" | "node-terminal" => "pwa-node",
533        "chrome" | "pwa-chrome" => "pwa-chrome",
534        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
535        _ => task_type_str,
536    }
537    .to_owned();
538
539    *task_type = Value::String(new_name);
540}