1use adapters::latest_github_release;
2use anyhow::Context as _;
3use collections::HashMap;
4use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
5use gpui::AsyncApp;
6use serde_json::Value;
7use std::{
8 borrow::Cow,
9 path::PathBuf,
10 sync::{LazyLock, OnceLock},
11};
12use task::DebugRequest;
13use util::{ResultExt, maybe};
14
15use crate::*;
16
17#[derive(Debug, Default)]
18pub struct JsDebugAdapter {
19 checked: OnceLock<()>,
20}
21
22impl JsDebugAdapter {
23 pub const ADAPTER_NAME: &'static str = "JavaScript";
24 const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
25 const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
26
27 async fn fetch_latest_adapter_version(
28 &self,
29 delegate: &Arc<dyn DapDelegate>,
30 ) -> Result<AdapterVersion> {
31 let release = latest_github_release(
32 &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
33 true,
34 false,
35 delegate.http_client(),
36 )
37 .await?;
38
39 let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
40
41 Ok(AdapterVersion {
42 tag_name: release.tag_name,
43 url: release
44 .assets
45 .iter()
46 .find(|asset| asset.name == asset_name)
47 .with_context(|| format!("no asset found matching {asset_name:?}"))?
48 .browser_download_url
49 .clone(),
50 })
51 }
52
53 async fn get_installed_binary(
54 &self,
55 delegate: &Arc<dyn DapDelegate>,
56 task_definition: &DebugTaskDefinition,
57 user_installed_path: Option<PathBuf>,
58 user_args: Option<Vec<String>>,
59 _: &mut AsyncApp,
60 ) -> Result<DebugAdapterBinary> {
61 let adapter_path = if let Some(user_installed_path) = user_installed_path {
62 user_installed_path
63 } else {
64 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
65
66 let file_name_prefix = format!("{}_", self.name());
67
68 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
69 file_name.starts_with(&file_name_prefix)
70 })
71 .await
72 .context("Couldn't find JavaScript dap directory")?
73 };
74
75 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
76 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
77
78 let mut envs = HashMap::default();
79
80 let mut configuration = task_definition.config.clone();
81 if let Some(configuration) = configuration.as_object_mut() {
82 maybe!({
83 configuration
84 .get("type")
85 .filter(|value| value == &"node-terminal")?;
86 let command = configuration.get("command")?.as_str()?.to_owned();
87 let mut args = shlex::split(&command)?.into_iter();
88 let program = args.next()?;
89 configuration.insert("runtimeExecutable".to_owned(), program.into());
90 configuration.insert(
91 "runtimeArgs".to_owned(),
92 args.map(Value::from).collect::<Vec<_>>().into(),
93 );
94 configuration.insert("console".to_owned(), "externalTerminal".into());
95 Some(())
96 });
97
98 configuration.entry("type").and_modify(normalize_task_type);
99
100 if let Some(program) = configuration
101 .get("program")
102 .cloned()
103 .and_then(|value| value.as_str().map(str::to_owned))
104 {
105 match program.as_str() {
106 "npm" | "pnpm" | "yarn" | "bun"
107 if !configuration.contains_key("runtimeExecutable")
108 && !configuration.contains_key("runtimeArgs") =>
109 {
110 configuration.remove("program");
111 configuration.insert("runtimeExecutable".to_owned(), program.into());
112 if let Some(args) = configuration.remove("args") {
113 configuration.insert("runtimeArgs".to_owned(), args);
114 }
115 }
116 _ => {}
117 }
118 }
119
120 if let Some(env) = configuration.get("env").cloned() {
121 if let Ok(env) = serde_json::from_value(env) {
122 envs = env;
123 }
124 }
125
126 configuration
127 .entry("cwd")
128 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
129
130 configuration
131 .entry("console")
132 .or_insert("externalTerminal".into());
133
134 configuration.entry("sourceMaps").or_insert(true.into());
135 configuration
136 .entry("pauseForSourceMap")
137 .or_insert(true.into());
138 configuration
139 .entry("sourceMapRenames")
140 .or_insert(true.into());
141 }
142
143 let arguments = if let Some(mut args) = user_args {
144 args.insert(
145 0,
146 adapter_path
147 .join(Self::ADAPTER_PATH)
148 .to_string_lossy()
149 .to_string(),
150 );
151 args
152 } else {
153 vec![
154 adapter_path
155 .join(Self::ADAPTER_PATH)
156 .to_string_lossy()
157 .to_string(),
158 port.to_string(),
159 host.to_string(),
160 ]
161 };
162
163 Ok(DebugAdapterBinary {
164 command: Some(
165 delegate
166 .node_runtime()
167 .binary_path()
168 .await?
169 .to_string_lossy()
170 .into_owned(),
171 ),
172 arguments,
173 cwd: Some(delegate.worktree_root_path().to_path_buf()),
174 envs,
175 connection: Some(adapters::TcpArguments {
176 host,
177 port,
178 timeout,
179 }),
180 request_args: StartDebuggingRequestArguments {
181 configuration,
182 request: self.request_kind(&task_definition.config).await?,
183 },
184 })
185 }
186}
187
188#[async_trait(?Send)]
189impl DebugAdapter for JsDebugAdapter {
190 fn name(&self) -> DebugAdapterName {
191 DebugAdapterName(Self::ADAPTER_NAME.into())
192 }
193
194 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
195 let mut args = json!({
196 "type": "pwa-node",
197 "request": match zed_scenario.request {
198 DebugRequest::Launch(_) => "launch",
199 DebugRequest::Attach(_) => "attach",
200 },
201 });
202
203 let map = args.as_object_mut().unwrap();
204 match &zed_scenario.request {
205 DebugRequest::Attach(attach) => {
206 map.insert("processId".into(), attach.process_id.into());
207 }
208 DebugRequest::Launch(launch) => {
209 if launch.program.starts_with("http://") {
210 map.insert("url".into(), launch.program.clone().into());
211 } else {
212 map.insert("program".into(), launch.program.clone().into());
213 }
214
215 if !launch.args.is_empty() {
216 map.insert("args".into(), launch.args.clone().into());
217 }
218 if !launch.env.is_empty() {
219 map.insert("env".into(), launch.env_json());
220 }
221
222 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
223 map.insert("stopOnEntry".into(), stop_on_entry.into());
224 }
225 if let Some(cwd) = launch.cwd.as_ref() {
226 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
227 }
228 }
229 };
230
231 Ok(DebugScenario {
232 adapter: zed_scenario.adapter,
233 label: zed_scenario.label,
234 build: None,
235 config: args,
236 tcp_connection: None,
237 })
238 }
239
240 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
241 static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
242 const RAW_SCHEMA: &str = include_str!("../schemas/JavaScript.json");
243 serde_json::from_str(RAW_SCHEMA).unwrap()
244 });
245 Cow::Borrowed(&*SCHEMA)
246 }
247
248 async fn get_binary(
249 &self,
250 delegate: &Arc<dyn DapDelegate>,
251 config: &DebugTaskDefinition,
252 user_installed_path: Option<PathBuf>,
253 user_args: Option<Vec<String>>,
254 cx: &mut AsyncApp,
255 ) -> Result<DebugAdapterBinary> {
256 if self.checked.set(()).is_ok() {
257 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
258 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
259 adapters::download_adapter_from_github(
260 Self::ADAPTER_NAME,
261 version,
262 adapters::DownloadedFileType::GzipTar,
263 paths::debug_adapters_dir(),
264 delegate.as_ref(),
265 )
266 .await?;
267 } else {
268 delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
269 }
270 }
271
272 self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
273 .await
274 }
275
276 fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
277 let label = args
278 .configuration
279 .get("name")?
280 .as_str()
281 .filter(|name| !name.is_empty())?;
282 Some(label.to_owned())
283 }
284}
285
286#[cfg(feature = "update-schemas")]
287impl JsDebugAdapter {
288 pub fn get_schema(
289 temp_dir: &tempfile::TempDir,
290 delegate: UpdateSchemasDapDelegate,
291 ) -> anyhow::Result<serde_json::Value> {
292 let (package_json, package_nls_json) = get_vsix_package_json(
293 temp_dir,
294 &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
295 |release| {
296 let version = release
297 .tag_name
298 .strip_prefix("v")
299 .context("parse version")?;
300 let asset_name = format!("ms-vscode.js-debug.{version}.vsix");
301 Ok(asset_name)
302 },
303 delegate,
304 )?;
305
306 let package_json = parse_package_json(package_json, package_nls_json)?;
307
308 let types = package_json
309 .contributes
310 .debuggers
311 .iter()
312 .map(|debugger| debugger.r#type.clone())
313 .collect::<Vec<_>>();
314 let mut conjuncts = package_json
315 .contributes
316 .debuggers
317 .into_iter()
318 .flat_map(|debugger| {
319 let r#type = debugger.r#type;
320 let configuration_attributes = debugger.configuration_attributes;
321 configuration_attributes
322 .launch
323 .map(|schema| ("launch", schema))
324 .into_iter()
325 .chain(
326 configuration_attributes
327 .attach
328 .map(|schema| ("attach", schema)),
329 )
330 .map(|(request, schema)| {
331 json!({
332 "if": {
333 "properties": {
334 "type": {
335 "const": r#type
336 },
337 "request": {
338 "const": request
339 }
340 },
341 "required": ["type", "request"]
342 },
343 "then": schema
344 })
345 })
346 .collect::<Vec<_>>()
347 })
348 .collect::<Vec<_>>();
349 conjuncts.push(json!({
350 "properties": {
351 "type": {
352 "enum": types
353 }
354 },
355 "required": ["type"]
356 }));
357 let schema = json!({
358 "allOf": conjuncts
359 });
360 Ok(schema)
361 }
362}
363
364fn normalize_task_type(task_type: &mut Value) {
365 let Some(task_type_str) = task_type.as_str() else {
366 return;
367 };
368
369 let new_name = match task_type_str {
370 "node" | "pwa-node" | "node-terminal" => "pwa-node",
371 "chrome" | "pwa-chrome" => "pwa-chrome",
372 "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
373 _ => task_type_str,
374 }
375 .to_owned();
376
377 *task_type = Value::String(new_name);
378}