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