javascript.rs

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