javascript.rs

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