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