1use adapters::latest_github_release;
2use anyhow::Context as _;
3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
4use fs::Fs;
5use gpui::{AsyncApp, BackgroundExecutor};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::{
9 borrow::Cow,
10 collections::HashMap,
11 path::{Path, PathBuf},
12 sync::{LazyLock, OnceLock},
13};
14use task::{DebugRequest, EnvVariableReplacer, VariableName};
15use tempfile::TempDir;
16use util::{ResultExt, maybe};
17
18use crate::*;
19
20#[derive(Debug, Default)]
21pub struct JsDebugAdapter {
22 checked: OnceLock<()>,
23}
24
25impl JsDebugAdapter {
26 pub const ADAPTER_NAME: &'static str = "JavaScript";
27 const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
28 const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
29
30 async fn fetch_latest_adapter_version(
31 &self,
32 delegate: &Arc<dyn DapDelegate>,
33 ) -> Result<AdapterVersion> {
34 let release = latest_github_release(
35 &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
36 true,
37 false,
38 delegate.http_client(),
39 )
40 .await?;
41
42 let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
43
44 Ok(AdapterVersion {
45 tag_name: release.tag_name,
46 url: release
47 .assets
48 .iter()
49 .find(|asset| asset.name == asset_name)
50 .with_context(|| format!("no asset found matching {asset_name:?}"))?
51 .browser_download_url
52 .clone(),
53 })
54 }
55
56 async fn get_installed_binary(
57 &self,
58 delegate: &Arc<dyn DapDelegate>,
59 task_definition: &DebugTaskDefinition,
60 user_installed_path: Option<PathBuf>,
61 user_args: Option<Vec<String>>,
62 _: &mut AsyncApp,
63 ) -> Result<DebugAdapterBinary> {
64 let adapter_path = if let Some(user_installed_path) = user_installed_path {
65 user_installed_path
66 } else {
67 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
68
69 let file_name_prefix = format!("{}_", self.name());
70
71 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
72 file_name.starts_with(&file_name_prefix)
73 })
74 .await
75 .context("Couldn't find JavaScript dap directory")?
76 };
77
78 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
79 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
80
81 let mut configuration = task_definition.config.clone();
82 if let Some(configuration) = configuration.as_object_mut() {
83 maybe!({
84 configuration
85 .get("type")
86 .filter(|value| value == &"node-terminal")?;
87 let command = configuration.get("command")?.as_str()?.to_owned();
88 let mut args = shlex::split(&command)?.into_iter();
89 let program = args.next()?;
90 configuration.insert("runtimeExecutable".to_owned(), program.into());
91 configuration.insert(
92 "runtimeArgs".to_owned(),
93 args.map(Value::from).collect::<Vec<_>>().into(),
94 );
95 configuration.insert("console".to_owned(), "externalTerminal".into());
96 Some(())
97 });
98
99 configuration.entry("type").and_modify(normalize_task_type);
100
101 if let Some(program) = configuration
102 .get("program")
103 .cloned()
104 .and_then(|value| value.as_str().map(str::to_owned))
105 {
106 match program.as_str() {
107 "npm" | "pnpm" | "yarn" | "bun"
108 if !configuration.contains_key("runtimeExecutable")
109 && !configuration.contains_key("runtimeArgs") =>
110 {
111 configuration.remove("program");
112 configuration.insert("runtimeExecutable".to_owned(), program.into());
113 if let Some(args) = configuration.remove("args") {
114 configuration.insert("runtimeArgs".to_owned(), args);
115 }
116 }
117 _ => {}
118 }
119 }
120
121 configuration
122 .entry("cwd")
123 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
124
125 configuration
126 .entry("console")
127 .or_insert("externalTerminal".into());
128
129 configuration.entry("sourceMaps").or_insert(true.into());
130 configuration
131 .entry("pauseForSourceMap")
132 .or_insert(true.into());
133 configuration
134 .entry("sourceMapRenames")
135 .or_insert(true.into());
136 }
137
138 let arguments = if let Some(mut args) = user_args {
139 args.insert(
140 0,
141 adapter_path
142 .join(Self::ADAPTER_PATH)
143 .to_string_lossy()
144 .to_string(),
145 );
146 args
147 } else {
148 vec![
149 adapter_path
150 .join(Self::ADAPTER_PATH)
151 .to_string_lossy()
152 .to_string(),
153 port.to_string(),
154 host.to_string(),
155 ]
156 };
157
158 Ok(DebugAdapterBinary {
159 command: Some(
160 delegate
161 .node_runtime()
162 .binary_path()
163 .await?
164 .to_string_lossy()
165 .into_owned(),
166 ),
167 arguments,
168 cwd: Some(delegate.worktree_root_path().to_path_buf()),
169 envs: HashMap::default(),
170 connection: Some(adapters::TcpArguments {
171 host,
172 port,
173 timeout,
174 }),
175 request_args: StartDebuggingRequestArguments {
176 configuration,
177 request: self.request_kind(&task_definition.config).await?,
178 },
179 })
180 }
181}
182
183#[async_trait(?Send)]
184impl DebugAdapter for JsDebugAdapter {
185 fn name(&self) -> DebugAdapterName {
186 DebugAdapterName(Self::ADAPTER_NAME.into())
187 }
188
189 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
190 let mut args = json!({
191 "type": "pwa-node",
192 "request": match zed_scenario.request {
193 DebugRequest::Launch(_) => "launch",
194 DebugRequest::Attach(_) => "attach",
195 },
196 });
197
198 let map = args.as_object_mut().unwrap();
199 match &zed_scenario.request {
200 DebugRequest::Attach(attach) => {
201 map.insert("processId".into(), attach.process_id.into());
202 }
203 DebugRequest::Launch(launch) => {
204 if launch.program.starts_with("http://") {
205 map.insert("url".into(), launch.program.clone().into());
206 } else {
207 map.insert("program".into(), launch.program.clone().into());
208 }
209
210 if !launch.args.is_empty() {
211 map.insert("args".into(), launch.args.clone().into());
212 }
213 if !launch.env.is_empty() {
214 map.insert("env".into(), launch.env_json());
215 }
216
217 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
218 map.insert("stopOnEntry".into(), stop_on_entry.into());
219 }
220 if let Some(cwd) = launch.cwd.as_ref() {
221 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
222 }
223 }
224 };
225
226 Ok(DebugScenario {
227 adapter: zed_scenario.adapter,
228 label: zed_scenario.label,
229 build: None,
230 config: args,
231 tcp_connection: None,
232 })
233 }
234
235 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
236 static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
237 const RAW_SCHEMA: &str = include_str!("../schemas/JavaScript.json");
238 serde_json::from_str(RAW_SCHEMA).unwrap()
239 });
240 Cow::Borrowed(&*SCHEMA)
241 }
242
243 async fn get_binary(
244 &self,
245 delegate: &Arc<dyn DapDelegate>,
246 config: &DebugTaskDefinition,
247 user_installed_path: Option<PathBuf>,
248 user_args: Option<Vec<String>>,
249 cx: &mut AsyncApp,
250 ) -> Result<DebugAdapterBinary> {
251 if self.checked.set(()).is_ok() {
252 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
253 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
254 adapters::download_adapter_from_github(
255 self.name(),
256 version,
257 adapters::DownloadedFileType::GzipTar,
258 paths::debug_adapters_dir(),
259 delegate.as_ref(),
260 )
261 .await?;
262 } else {
263 delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
264 }
265 }
266
267 self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
268 .await
269 }
270
271 fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
272 let label = args
273 .configuration
274 .get("name")?
275 .as_str()
276 .filter(|name| !name.is_empty())?;
277 Some(label.to_owned())
278 }
279}
280
281#[cfg(feature = "update-schemas")]
282impl JsDebugAdapter {
283 pub fn get_schema(
284 temp_dir: &TempDir,
285 output_dir: &Path,
286 executor: BackgroundExecutor,
287 ) -> anyhow::Result<()> {
288 #[derive(Serialize, Deserialize)]
289 struct PackageJsonConfigurationAttributes {
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 launch: Option<serde_json::Value>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 attach: Option<serde_json::Value>,
294 }
295
296 #[derive(Serialize, Deserialize)]
297 #[serde(rename_all = "camelCase")]
298 struct PackageJsonDebugger {
299 r#type: String,
300 configuration_attributes: PackageJsonConfigurationAttributes,
301 }
302
303 #[derive(Serialize, Deserialize)]
304 struct PackageJsonContributes {
305 debuggers: Vec<PackageJsonDebugger>,
306 }
307
308 #[derive(Serialize, Deserialize)]
309 struct PackageJson {
310 contributes: PackageJsonContributes,
311 }
312
313 let temp_dir = std::fs::canonicalize(temp_dir.path())?;
314 let delegate = UpdateSchemasDapDelegate::new(executor.clone());
315 let fs = delegate.fs.clone();
316 let client = delegate.client.clone();
317
318 let (package_json, package_nls_json) = executor.block(async move {
319 let release = latest_github_release(
320 &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
321 true,
322 false,
323 client.clone(),
324 )
325 .await?;
326
327 let version = release
328 .tag_name
329 .strip_prefix("v")
330 .context("parse version")?;
331 let asset_name = format!("ms-vscode.js-debug.{version}.vsix",);
332 let version = AdapterVersion {
333 tag_name: release.tag_name,
334 url: release
335 .assets
336 .iter()
337 .find(|asset| asset.name == asset_name)
338 .with_context(|| format!("no asset found matching {asset_name:?}"))?
339 .browser_download_url
340 .clone(),
341 };
342
343 let path = adapters::download_adapter_from_github(
344 DebugAdapterName(Self::ADAPTER_NAME.into()),
345 version,
346 adapters::DownloadedFileType::Vsix,
347 &temp_dir,
348 &delegate,
349 )
350 .await?;
351 let package_json = fs
352 .load(&path.join("extension").join("package.json"))
353 .await?;
354 let package_nls_json = fs
355 .load(&path.join("extension").join("package.nls.json"))
356 .await?;
357 anyhow::Ok((package_json, package_nls_json))
358 })?;
359
360 let package_nls_json =
361 serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_nls_json)?
362 .into_iter()
363 .filter_map(|(k, v)| {
364 let v = v.as_str()?;
365 Some((k, v.to_owned()))
366 })
367 .collect();
368
369 let package_json: serde_json::Value = serde_json::from_str(&package_json)?;
370
371 struct Replacer {
372 package_nls_json: HashMap<String, String>,
373 env: EnvVariableReplacer,
374 }
375
376 impl Replacer {
377 fn replace(&self, input: serde_json::Value) -> serde_json::Value {
378 match input {
379 serde_json::Value::String(s) => {
380 if s.starts_with("%") && s.ends_with("%") {
381 self.package_nls_json
382 .get(s.trim_matches('%'))
383 .map(|s| s.as_str().into())
384 .unwrap_or("(missing)".into())
385 } else {
386 self.env.replace(&s).into()
387 }
388 }
389 serde_json::Value::Array(arr) => {
390 serde_json::Value::Array(arr.into_iter().map(|v| self.replace(v)).collect())
391 }
392 serde_json::Value::Object(obj) => serde_json::Value::Object(
393 obj.into_iter().map(|(k, v)| (k, self.replace(v))).collect(),
394 ),
395 _ => input,
396 }
397 }
398 }
399
400 let env = EnvVariableReplacer::new(HashMap::from_iter([(
401 "workspaceFolder".to_owned(),
402 VariableName::WorktreeRoot.to_string(),
403 )]));
404 let replacer = Replacer {
405 env,
406 package_nls_json,
407 };
408 let package_json = replacer.replace(package_json);
409
410 let package_json: PackageJson = serde_json::from_value(package_json)?;
411
412 let types = package_json
413 .contributes
414 .debuggers
415 .iter()
416 .map(|debugger| debugger.r#type.clone())
417 .collect::<Vec<_>>();
418 let mut conjuncts = package_json
419 .contributes
420 .debuggers
421 .into_iter()
422 .flat_map(|debugger| {
423 let r#type = debugger.r#type;
424 let configuration_attributes = debugger.configuration_attributes;
425 configuration_attributes
426 .launch
427 .map(|schema| ("launch", schema))
428 .into_iter()
429 .chain(
430 configuration_attributes
431 .attach
432 .map(|schema| ("attach", schema)),
433 )
434 .map(|(request, schema)| {
435 json!({
436 "if": {
437 "properties": {
438 "type": {
439 "const": r#type
440 },
441 "request": {
442 "const": request
443 }
444 },
445 "required": ["type", "request"]
446 },
447 "then": schema
448 })
449 })
450 .collect::<Vec<_>>()
451 })
452 .collect::<Vec<_>>();
453 conjuncts.push(json!({
454 "properties": {
455 "type": {
456 "enum": types
457 }
458 },
459 "required": ["type"]
460 }));
461 let schema = json!({
462 "allOf": conjuncts
463 });
464
465 // FIXME figure out what to do about formatting
466 let mut schema = serde_json::to_string_pretty(&schema)?;
467 schema.push('\n');
468 std::fs::write(
469 output_dir.join(Self::ADAPTER_NAME).with_extension("json"),
470 schema,
471 )?;
472 Ok(())
473 }
474}
475
476fn normalize_task_type(task_type: &mut Value) {
477 let Some(task_type_str) = task_type.as_str() else {
478 return;
479 };
480
481 let new_name = match task_type_str {
482 "node" | "pwa-node" | "node-terminal" => "pwa-node",
483 "chrome" | "pwa-chrome" => "pwa-chrome",
484 "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
485 _ => task_type_str,
486 }
487 .to_owned();
488
489 *task_type = Value::String(new_name);
490}