javascript.rs

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