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 if !launch.env.is_empty() {
40 map.insert("env".into(), launch.env_json());
41 }
42
43 if let Some(stop_on_entry) = config.stop_on_entry {
44 map.insert("stopOnEntry".into(), stop_on_entry.into());
45 }
46 if let Some(cwd) = launch.cwd.as_ref() {
47 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
48 }
49 }
50 }
51 StartDebuggingRequestArguments {
52 configuration: args,
53 request: config.request.to_dap(),
54 }
55 }
56
57 async fn fetch_latest_adapter_version(
58 &self,
59 delegate: &dyn DapDelegate,
60 ) -> Result<AdapterVersion> {
61 let release = latest_github_release(
62 &format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
63 true,
64 false,
65 delegate.http_client(),
66 )
67 .await?;
68
69 let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
70
71 Ok(AdapterVersion {
72 tag_name: release.tag_name,
73 url: release
74 .assets
75 .iter()
76 .find(|asset| asset.name == asset_name)
77 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
78 .browser_download_url
79 .clone(),
80 })
81 }
82
83 async fn get_installed_binary(
84 &self,
85 delegate: &dyn DapDelegate,
86 config: &DebugTaskDefinition,
87 user_installed_path: Option<PathBuf>,
88 _: &mut AsyncApp,
89 ) -> Result<DebugAdapterBinary> {
90 let adapter_path = if let Some(user_installed_path) = user_installed_path {
91 user_installed_path
92 } else {
93 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
94
95 let file_name_prefix = format!("{}_", self.name());
96
97 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
98 file_name.starts_with(&file_name_prefix)
99 })
100 .await
101 .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
102 };
103
104 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
105 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
106
107 Ok(DebugAdapterBinary {
108 command: delegate
109 .node_runtime()
110 .binary_path()
111 .await?
112 .to_string_lossy()
113 .into_owned(),
114 arguments: vec![
115 adapter_path
116 .join(Self::ADAPTER_PATH)
117 .to_string_lossy()
118 .to_string(),
119 port.to_string(),
120 host.to_string(),
121 ],
122 cwd: None,
123 envs: HashMap::default(),
124 connection: Some(adapters::TcpArguments {
125 host,
126 port,
127 timeout,
128 }),
129 request_args: self.request_args(config),
130 })
131 }
132}
133
134#[async_trait(?Send)]
135impl DebugAdapter for JsDebugAdapter {
136 fn name(&self) -> DebugAdapterName {
137 DebugAdapterName(Self::ADAPTER_NAME.into())
138 }
139
140 async fn get_binary(
141 &self,
142 delegate: &dyn DapDelegate,
143 config: &DebugTaskDefinition,
144 user_installed_path: Option<PathBuf>,
145 cx: &mut AsyncApp,
146 ) -> Result<DebugAdapterBinary> {
147 if self.checked.set(()).is_ok() {
148 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
149 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
150 adapters::download_adapter_from_github(
151 self.name(),
152 version,
153 adapters::DownloadedFileType::GzipTar,
154 delegate,
155 )
156 .await?;
157 }
158 }
159
160 self.get_installed_binary(delegate, &config, user_installed_path, cx)
161 .await
162 }
163}