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}