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