1use adapters::latest_github_release;
2use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
3use gpui::AsyncApp;
4use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
5use task::DebugRequest;
6use util::ResultExt;
7
8use crate::*;
9
10#[derive(Debug, Default)]
11pub(crate) struct JsDebugAdapter {
12 checked: OnceLock<()>,
13}
14
15impl JsDebugAdapter {
16 const ADAPTER_NAME: &'static str = "JavaScript";
17 const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
18 const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
19
20 fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
21 let mut args = json!({
22 "type": "pwa-node",
23 "request": match config.request {
24 DebugRequest::Launch(_) => "launch",
25 DebugRequest::Attach(_) => "attach",
26 },
27 });
28 let map = args.as_object_mut().unwrap();
29 match &config.request {
30 DebugRequest::Attach(attach) => {
31 map.insert("processId".into(), attach.process_id.into());
32 }
33 DebugRequest::Launch(launch) => {
34 map.insert("program".into(), launch.program.clone().into());
35
36 if !launch.args.is_empty() {
37 map.insert("args".into(), launch.args.clone().into());
38 }
39
40 if let Some(stop_on_entry) = config.stop_on_entry {
41 map.insert("stopOnEntry".into(), stop_on_entry.into());
42 }
43 if let Some(cwd) = launch.cwd.as_ref() {
44 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
45 }
46 }
47 }
48 StartDebuggingRequestArguments {
49 configuration: args,
50 request: config.request.to_dap(),
51 }
52 }
53
54 async fn fetch_latest_adapter_version(
55 &self,
56 delegate: &dyn DapDelegate,
57 ) -> Result<AdapterVersion> {
58 let release = latest_github_release(
59 &format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
60 true,
61 false,
62 delegate.http_client(),
63 )
64 .await?;
65
66 let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
67
68 Ok(AdapterVersion {
69 tag_name: release.tag_name,
70 url: release
71 .assets
72 .iter()
73 .find(|asset| asset.name == asset_name)
74 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
75 .browser_download_url
76 .clone(),
77 })
78 }
79
80 async fn get_installed_binary(
81 &self,
82 delegate: &dyn DapDelegate,
83 config: &DebugTaskDefinition,
84 user_installed_path: Option<PathBuf>,
85 _: &mut AsyncApp,
86 ) -> Result<DebugAdapterBinary> {
87 let adapter_path = if let Some(user_installed_path) = user_installed_path {
88 user_installed_path
89 } else {
90 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
91
92 let file_name_prefix = format!("{}_", self.name());
93
94 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
95 file_name.starts_with(&file_name_prefix)
96 })
97 .await
98 .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
99 };
100
101 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
102 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
103
104 Ok(DebugAdapterBinary {
105 command: delegate
106 .node_runtime()
107 .binary_path()
108 .await?
109 .to_string_lossy()
110 .into_owned(),
111 arguments: vec![
112 adapter_path
113 .join(Self::ADAPTER_PATH)
114 .to_string_lossy()
115 .to_string(),
116 port.to_string(),
117 host.to_string(),
118 ],
119 cwd: None,
120 envs: HashMap::default(),
121 connection: Some(adapters::TcpArguments {
122 host,
123 port,
124 timeout,
125 }),
126 request_args: self.request_args(config),
127 })
128 }
129}
130
131#[async_trait(?Send)]
132impl DebugAdapter for JsDebugAdapter {
133 fn name(&self) -> DebugAdapterName {
134 DebugAdapterName(Self::ADAPTER_NAME.into())
135 }
136
137 async fn get_binary(
138 &self,
139 delegate: &dyn DapDelegate,
140 config: &DebugTaskDefinition,
141 user_installed_path: Option<PathBuf>,
142 cx: &mut AsyncApp,
143 ) -> Result<DebugAdapterBinary> {
144 if self.checked.set(()).is_ok() {
145 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
146 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
147 adapters::download_adapter_from_github(
148 self.name(),
149 version,
150 adapters::DownloadedFileType::GzipTar,
151 delegate,
152 )
153 .await?;
154 }
155 }
156
157 self.get_installed_binary(delegate, &config, user_installed_path, cx)
158 .await
159 }
160}