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