1use std::collections::{HashMap, HashSet};
4use std::path::Path;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DebugAdapterEntry {
11 pub name: String,
13 pub language_ids: Vec<String>,
15 pub command: String,
17 pub args: Vec<String>,
19 pub install_instructions: HashMap<String, String>,
21 pub homepage: String,
23}
24
25pub struct DebugAdapterRegistry {
31 entries: Vec<DebugAdapterEntry>,
32}
33
34impl DebugAdapterRegistry {
35 #[must_use]
37 pub fn with_builtins() -> Self {
38 Self {
39 entries: builtin_entries(),
40 }
41 }
42
43 #[must_use]
45 pub const fn empty() -> Self {
46 Self {
47 entries: Vec::new(),
48 }
49 }
50
51 pub fn register(&mut self, entry: DebugAdapterEntry) {
53 self.entries.push(entry);
54 }
55
56 #[must_use]
58 pub fn find_by_language(&self, language_id: &str) -> Vec<&DebugAdapterEntry> {
59 self.entries
60 .iter()
61 .filter(|e| e.language_ids.iter().any(|l| l == language_id))
62 .collect()
63 }
64
65 #[must_use]
67 pub fn find_by_name(&self, name: &str) -> Option<&DebugAdapterEntry> {
68 self.entries.iter().find(|e| e.name == name)
69 }
70
71 #[must_use]
73 pub fn all(&self) -> &[DebugAdapterEntry] {
74 &self.entries
75 }
76
77 #[must_use]
79 pub fn is_available(&self, name: &str) -> bool {
80 self.find_by_name(name)
81 .is_some_and(|entry| which::which(&entry.command).is_ok())
82 }
83
84 #[must_use]
93 pub fn detect_for_project(&self, project_root: &Path) -> Vec<&DebugAdapterEntry> {
94 let extensions = collect_extensions(project_root, 2);
95 let mut language_ids = HashSet::new();
96 for ext in &extensions {
97 if let Some(lang) = extension_to_language_id(ext) {
98 let _inserted = language_ids.insert(lang);
99 }
100 }
101
102 let mut seen = HashSet::new();
103 let mut result = Vec::new();
104 for lang_id in &language_ids {
105 for entry in self.find_by_language(lang_id) {
106 if seen.insert(&entry.name) && which::which(&entry.command).is_ok() {
107 result.push(entry);
108 }
109 }
110 }
111 result
112 }
113}
114
115impl Default for DebugAdapterRegistry {
116 fn default() -> Self {
117 Self::with_builtins()
118 }
119}
120
121fn extension_to_language_id(ext: &str) -> Option<&'static str> {
125 match ext {
126 "rs" => Some("rust"),
127 "go" => Some("go"),
128 "py" | "pyi" => Some("python"),
129 "js" | "mjs" | "cjs" => Some("javascript"),
130 "ts" | "mts" | "cts" | "tsx" => Some("typescript"),
131 "java" => Some("java"),
132 "c" | "h" => Some("c"),
133 "cpp" | "cxx" | "cc" | "hpp" | "hxx" => Some("cpp"),
134 _ => None,
135 }
136}
137
138const SKIP_DIRS: &[&str] = &[
140 "node_modules",
141 "target",
142 ".git",
143 ".hg",
144 ".svn",
145 "__pycache__",
146 ".mypy_cache",
147 ".pytest_cache",
148 ".tox",
149 ".venv",
150 "venv",
151 ".env",
152 "vendor",
153 "dist",
154 "build",
155 "out",
156 ".next",
157 ".nuxt",
158 "coverage",
159 ".cargo",
160 ".rustup",
161];
162
163fn collect_extensions(root: &Path, max_depth: usize) -> Vec<String> {
168 let mut extensions = HashSet::new();
169 collect_extensions_recursive(root, max_depth, 0, &mut extensions);
170 extensions.into_iter().collect()
171}
172
173fn collect_extensions_recursive(
175 dir: &Path,
176 max_depth: usize,
177 current_depth: usize,
178 extensions: &mut HashSet<String>,
179) {
180 let Ok(entries) = std::fs::read_dir(dir) else {
181 return;
182 };
183
184 for entry in entries {
185 let Ok(entry) = entry else { continue };
186
187 let Ok(file_type) = entry.file_type() else {
188 continue;
189 };
190
191 let name = entry.file_name();
192 let name_str = name.to_string_lossy();
193
194 if file_type.is_dir() {
195 if name_str.starts_with('.') || SKIP_DIRS.contains(&name_str.as_ref()) {
196 continue;
197 }
198 if current_depth < max_depth {
199 collect_extensions_recursive(
200 &entry.path(),
201 max_depth,
202 current_depth + 1,
203 extensions,
204 );
205 }
206 } else if file_type.is_file()
207 && let Some(ext) = entry.path().extension().and_then(|e| e.to_str())
208 {
209 let _inserted = extensions.insert(ext.to_owned());
210 }
211 }
212}
213
214#[allow(clippy::too_many_lines)] fn builtin_entries() -> Vec<DebugAdapterEntry> {
217 vec![
218 DebugAdapterEntry {
219 name: "codelldb".into(),
220 language_ids: vec!["rust".into(), "c".into(), "cpp".into()],
221 command: "codelldb".into(),
222 args: vec!["--port".into(), "0".into()],
223 install_instructions: {
224 let mut m = HashMap::new();
225 let _ = m.insert(
226 "linux".into(),
227 "Download from https://github.com/vadimcn/codelldb/releases".into(),
228 );
229 let _ = m.insert(
230 "macos".into(),
231 "Download from https://github.com/vadimcn/codelldb/releases".into(),
232 );
233 let _ = m.insert(
234 "windows".into(),
235 "Download from https://github.com/vadimcn/codelldb/releases".into(),
236 );
237 m
238 },
239 homepage: "https://github.com/vadimcn/codelldb".into(),
240 },
241 DebugAdapterEntry {
242 name: "dlv-dap".into(),
243 language_ids: vec!["go".into()],
244 command: "dlv".into(),
245 args: vec!["dap".into()],
246 install_instructions: {
247 let mut m = HashMap::new();
248 let _ = m.insert(
249 "linux".into(),
250 "go install github.com/go-delve/delve/cmd/dlv@latest".into(),
251 );
252 let _ = m.insert(
253 "macos".into(),
254 "go install github.com/go-delve/delve/cmd/dlv@latest".into(),
255 );
256 let _ = m.insert(
257 "windows".into(),
258 "go install github.com/go-delve/delve/cmd/dlv@latest".into(),
259 );
260 m
261 },
262 homepage: "https://github.com/go-delve/delve".into(),
263 },
264 DebugAdapterEntry {
265 name: "debugpy".into(),
266 language_ids: vec!["python".into()],
267 command: "python".into(),
268 args: vec!["-m".into(), "debugpy.adapter".into()],
269 install_instructions: {
270 let mut m = HashMap::new();
271 let _ = m.insert("linux".into(), "pip install debugpy".into());
272 let _ = m.insert("macos".into(), "pip install debugpy".into());
273 let _ = m.insert("windows".into(), "pip install debugpy".into());
274 m
275 },
276 homepage: "https://github.com/microsoft/debugpy".into(),
277 },
278 DebugAdapterEntry {
279 name: "js-debug".into(),
280 language_ids: vec!["javascript".into(), "typescript".into(), "node".into()],
281 command: "js-debug-adapter".into(),
282 args: Vec::new(),
283 install_instructions: {
284 let mut m = HashMap::new();
285 let _ = m.insert("linux".into(), "npm install -g @vscode/js-debug".into());
286 let _ = m.insert("macos".into(), "npm install -g @vscode/js-debug".into());
287 let _ = m.insert("windows".into(), "npm install -g @vscode/js-debug".into());
288 m
289 },
290 homepage: "https://github.com/microsoft/vscode-js-debug".into(),
291 },
292 DebugAdapterEntry {
293 name: "java-debug".into(),
294 language_ids: vec!["java".into()],
295 command: "java".into(),
296 args: vec!["-agentlib:jdwp=transport=dt_socket,server=y,suspend=n".into()],
297 install_instructions: {
298 let mut m = HashMap::new();
299 let _ = m.insert(
300 "linux".into(),
301 "Install via JDT.LS: https://github.com/microsoft/java-debug".into(),
302 );
303 let _ = m.insert(
304 "macos".into(),
305 "Install via JDT.LS: https://github.com/microsoft/java-debug".into(),
306 );
307 let _ = m.insert(
308 "windows".into(),
309 "Install via JDT.LS: https://github.com/microsoft/java-debug".into(),
310 );
311 m
312 },
313 homepage: "https://github.com/microsoft/java-debug".into(),
314 },
315 ]
316}
317
318#[cfg(test)]
319#[allow(clippy::unwrap_used)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn builtin_registry_has_five_entries() {
325 let registry = DebugAdapterRegistry::with_builtins();
326 assert_eq!(registry.all().len(), 5);
327 }
328
329 #[test]
330 fn find_by_language_rust() {
331 let registry = DebugAdapterRegistry::with_builtins();
332 let adapters = registry.find_by_language("rust");
333 assert_eq!(adapters.len(), 1);
334 assert_eq!(adapters[0].name, "codelldb");
335 }
336
337 #[test]
338 fn find_by_language_go() {
339 let registry = DebugAdapterRegistry::with_builtins();
340 let adapters = registry.find_by_language("go");
341 assert_eq!(adapters.len(), 1);
342 assert_eq!(adapters[0].name, "dlv-dap");
343 }
344
345 #[test]
346 fn find_by_name() {
347 let registry = DebugAdapterRegistry::with_builtins();
348 assert!(registry.find_by_name("debugpy").is_some());
349 assert!(registry.find_by_name("nonexistent").is_none());
350 }
351
352 #[test]
353 fn extension_to_language_id_known() {
354 assert_eq!(super::extension_to_language_id("rs"), Some("rust"));
355 assert_eq!(super::extension_to_language_id("go"), Some("go"));
356 assert_eq!(super::extension_to_language_id("py"), Some("python"));
357 assert_eq!(super::extension_to_language_id("ts"), Some("typescript"));
358 assert_eq!(super::extension_to_language_id("cpp"), Some("cpp"));
359 }
360
361 #[test]
362 fn extension_to_language_id_unknown() {
363 assert_eq!(super::extension_to_language_id("xyz"), None);
364 assert_eq!(super::extension_to_language_id(""), None);
365 }
366
367 #[test]
368 fn detect_for_project_returns_empty_for_nonexistent_dir() {
369 let registry = DebugAdapterRegistry::with_builtins();
370 let result = registry.detect_for_project(std::path::Path::new("/nonexistent/path/12345"));
371 assert!(result.is_empty());
372 }
373
374 #[test]
375 fn collect_extensions_from_temp_dir() {
376 let dir = std::env::temp_dir().join("synwire_dap_test_collect_ext");
377 let _ = std::fs::remove_dir_all(&dir);
378 std::fs::create_dir_all(dir.join("src")).unwrap();
379 std::fs::write(dir.join("main.go"), "package main").unwrap();
380 std::fs::write(dir.join("src/helper.py"), "").unwrap();
381
382 let exts = super::collect_extensions(&dir, 2);
383 assert!(exts.contains(&"go".to_owned()));
384 assert!(exts.contains(&"py".to_owned()));
385
386 let _ = std::fs::remove_dir_all(&dir);
387 }
388
389 #[test]
390 fn register_custom_entry() {
391 let mut registry = DebugAdapterRegistry::empty();
392 assert!(registry.all().is_empty());
393
394 registry.register(DebugAdapterEntry {
395 name: "my-adapter".into(),
396 language_ids: vec!["lua".into()],
397 command: "lua-debug".into(),
398 args: Vec::new(),
399 install_instructions: HashMap::new(),
400 homepage: "https://example.com".into(),
401 });
402
403 assert_eq!(registry.all().len(), 1);
404 let found = registry.find_by_language("lua");
405 assert_eq!(found.len(), 1);
406 }
407}