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                    envs = env;
105                }
106
107            configuration
108                .entry("cwd")
109                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
110
111            configuration
112                .entry("console")
113                .or_insert("externalTerminal".into());
114
115            configuration.entry("sourceMaps").or_insert(true.into());
116            configuration
117                .entry("pauseForSourceMap")
118                .or_insert(true.into());
119            configuration
120                .entry("sourceMapRenames")
121                .or_insert(true.into());
122        }
123
124        let adapter_path = if let Some(user_installed_path) = user_installed_path {
125            user_installed_path
126        } else {
127            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
128
129            let file_name_prefix = format!("{}_", self.name());
130
131            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
132                file_name.starts_with(&file_name_prefix)
133            })
134            .await
135            .context("Couldn't find JavaScript dap directory")?
136            .join(Self::ADAPTER_PATH)
137        };
138
139        let arguments = if let Some(mut args) = user_args {
140            args.insert(0, adapter_path.to_string_lossy().to_string());
141            args
142        } else {
143            vec![
144                adapter_path.to_string_lossy().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,
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", "node-terminal"],
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                                "attachSimplePort": {
286                                    "type": "number",
287                                    "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."
288                                },
289                                "runtimeExecutable": {
290                                    "type": ["string", "null"],
291                                    "description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
292                                    "default": "node"
293                                },
294                                "runtimeArgs": {
295                                    "type": ["array", "null"],
296                                    "description": "Arguments passed to the runtime executable",
297                                    "items": {
298                                        "type": "string"
299                                    },
300                                    "default": []
301                                },
302                                "outFiles": {
303                                    "type": "array",
304                                    "description": "Glob patterns for locating generated JavaScript files",
305                                    "items": {
306                                        "type": "string"
307                                    },
308                                    "default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
309                                },
310                                "sourceMaps": {
311                                    "type": "boolean",
312                                    "description": "Use JavaScript source maps if they exist",
313                                    "default": true
314                                },
315                                "pauseForSourceMap": {
316                                    "type": "boolean",
317                                    "description": "Wait for source maps to load before setting breakpoints.",
318                                    "default": true
319                                },
320                                "sourceMapRenames": {
321                                    "type": "boolean",
322                                    "description": "Whether to use the \"names\" mapping in sourcemaps.",
323                                    "default": true
324                                },
325                                "sourceMapPathOverrides": {
326                                    "type": "object",
327                                    "description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
328                                    "default": {}
329                                },
330                                "restart": {
331                                    "type": ["boolean", "object"],
332                                    "description": "Restart session after Node.js has terminated",
333                                    "default": false
334                                },
335                                "trace": {
336                                    "type": ["boolean", "object"],
337                                    "description": "Enables logging of the Debug Adapter",
338                                    "default": false
339                                },
340                                "console": {
341                                    "type": "string",
342                                    "enum": ["internalConsole", "integratedTerminal"],
343                                    "description": "Where to launch the debug target",
344                                    "default": "internalConsole"
345                                },
346                                // Browser-specific
347                                "url": {
348                                    "type": ["string", "null"],
349                                    "description": "Will navigate to this URL and attach to it (browser debugging)"
350                                },
351                                "webRoot": {
352                                    "type": "string",
353                                    "description": "Workspace absolute path to the webserver root",
354                                    "default": "${ZED_WORKTREE_ROOT}"
355                                },
356                                "userDataDir": {
357                                    "type": ["string", "boolean"],
358                                    "description": "Path to a custom Chrome user profile (browser debugging)",
359                                    "default": true
360                                },
361                                "skipFiles": {
362                                    "type": "array",
363                                    "description": "An array of glob patterns for files to skip when debugging",
364                                    "items": {
365                                        "type": "string"
366                                    },
367                                    "default": ["<node_internals>/**"]
368                                },
369                                "timeout": {
370                                    "type": "number",
371                                    "description": "Retry for this number of milliseconds to connect to the debug adapter",
372                                    "default": 10000
373                                },
374                                "resolveSourceMapLocations": {
375                                    "type": ["array", "null"],
376                                    "description": "A list of minimatch patterns for source map resolution",
377                                    "items": {
378                                        "type": "string"
379                                    }
380                                }
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
522            .configuration
523            .get("name")?
524            .as_str()
525            .filter(|name| !name.is_empty())?;
526        Some(label.to_owned())
527    }
528
529    fn compact_child_session(&self) -> bool {
530        true
531    }
532
533    fn prefer_thread_name(&self) -> bool {
534        true
535    }
536}
537
538fn normalize_task_type(task_type: &mut Value) {
539    let Some(task_type_str) = task_type.as_str() else {
540        return;
541    };
542
543    let new_name = match task_type_str {
544        "node" | "pwa-node" | "node-terminal" => "pwa-node",
545        "chrome" | "pwa-chrome" => "pwa-chrome",
546        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
547        _ => task_type_str,
548    }
549    .to_owned();
550
551    *task_type = Value::String(new_name);
552}