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