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