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