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