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