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