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}