Skip to main content

scope_engine/
treesitter.rs

1use std::path::{Path, PathBuf};
2
3use tree_sitter::StreamingIterator;
4
5use crate::language::LanguageRegistry;
6use crate::selector::{ParsedSelector, SelectorTarget, SymbolKind, SymbolSelector};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SymbolMatch {
10    pub name: String,
11    pub kind: SymbolKind,
12    pub kind_prefix: &'static str,
13    pub start_line: usize,
14    pub end_line: usize,
15}
16
17fn normalize_path_for_cmp(p: &Path) -> PathBuf {
18    let s = p.to_string_lossy();
19    if let Some(rest) = s.strip_prefix(r"\\?\") {
20        PathBuf::from(rest)
21    } else {
22        p.to_path_buf()
23    }
24}
25
26impl SymbolMatch {
27    pub fn canonical_selector(&self, file_path: &Path, project_root: &Path) -> String {
28        let rel_path = normalize_path_for_cmp(file_path)
29            .strip_prefix(normalize_path_for_cmp(project_root))
30            .ok()
31            .map(|p| p.to_string_lossy().to_string())
32            .unwrap_or_else(|| file_path.to_string_lossy().to_string())
33            .replace('\\', "/");
34
35        format!(
36            "{}::{}{} #L{}-L{}",
37            rel_path, self.kind_prefix, self.name, self.start_line, self.end_line
38        )
39    }
40
41    pub fn source_from(&self, content: &str) -> String {
42        let lines: Vec<&str> = content.lines().collect();
43        if self.start_line == 0 || self.end_line < self.start_line || self.start_line > lines.len()
44        {
45            return String::new();
46        }
47
48        let start_idx = self.start_line - 1;
49        let end_idx = self.end_line.min(lines.len());
50        let mut snippet = lines[start_idx..end_idx].join("\n");
51        if content.ends_with('\n') || self.end_line < lines.len() {
52            snippet.push('\n');
53        }
54        snippet
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ParseErrorDiagnostic {
60    pub kind: ParseErrorKind,
61    pub node_kind: String,
62    pub start_line: usize,
63    pub start_column: usize,
64    pub end_line: usize,
65    pub end_column: usize,
66    pub snippet: String,
67}
68
69impl ParseErrorDiagnostic {
70    pub fn message(&self) -> String {
71        format!(
72            "first parse error: {} node `{}` at L{}:C{}-L{}:C{}\n{}",
73            self.kind.as_str(),
74            self.node_kind,
75            self.start_line,
76            self.start_column,
77            self.end_line,
78            self.end_column,
79            self.snippet
80        )
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ParseErrorKind {
86    Error,
87    Missing,
88}
89
90impl ParseErrorKind {
91    fn as_str(self) -> &'static str {
92        match self {
93            Self::Error => "ERROR",
94            Self::Missing => "MISSING",
95        }
96    }
97}
98
99pub struct TreeSitterAnalyzer {
100    registry: LanguageRegistry,
101}
102
103impl Default for TreeSitterAnalyzer {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl TreeSitterAnalyzer {
110    pub fn new() -> Self {
111        Self {
112            registry: LanguageRegistry::new(),
113        }
114    }
115
116    /// Given a file path and a 1-based line number, find the innermost
117    /// named definition (function, struct, enum, trait, impl) that contains
118    /// that line. Returns a canonical CodeStruct-style selector like
119    /// `src/foo.rs::fn authenticate #L10-L20`.
120    pub fn find_containing_symbol(
121        &self,
122        file_path: &Path,
123        line_number: usize,
124        project_root: &Path,
125    ) -> Option<String> {
126        self.find_containing_symbol_match(file_path, line_number)
127            .map(|m| m.canonical_selector(file_path, project_root))
128    }
129
130    pub fn find_containing_symbol_match(
131        &self,
132        file_path: &Path,
133        line_number: usize,
134    ) -> Option<SymbolMatch> {
135        let symbols = self.symbols_in_file(file_path).ok()?;
136        symbols
137            .into_iter()
138            .filter(|m| line_number >= m.start_line && line_number <= m.end_line)
139            .max_by_key(|m| (m.start_line, usize::MAX - m.end_line))
140    }
141
142    pub fn resolve_selector(
143        &self,
144        file_path: &Path,
145        parsed: &ParsedSelector,
146    ) -> Result<SymbolMatch, String> {
147        let symbols = self.symbols_in_file(file_path)?;
148        let mut matches: Vec<SymbolMatch> = symbols
149            .into_iter()
150            .filter(|m| symbol_matches_selector(m, parsed))
151            .collect();
152
153        let symbol = match &parsed.target {
154            SelectorTarget::Symbol(symbol) => symbol,
155            _ => {
156                return Err(format!(
157                    "selector target is not a symbol and cannot be resolved as a symbol: {}",
158                    file_path.display()
159                ));
160            }
161        };
162
163        if let Some((start, end)) = symbol.line_range {
164            matches.retain(|m| m.start_line == start && m.end_line == end);
165        }
166
167        match matches.len() {
168            0 => Err(format!(
169                "symbol '{}' not found in {}",
170                symbol.name,
171                file_path.display()
172            )),
173            1 => Ok(matches.remove(0)),
174            _ => {
175                let candidates = matches
176                    .iter()
177                    .map(|m| {
178                        format!(
179                            "{}{} #L{}-L{}",
180                            m.kind_prefix, m.name, m.start_line, m.end_line
181                        )
182                    })
183                    .collect::<Vec<_>>()
184                    .join(", ");
185                Err(format!(
186                    "ambiguous selector for '{}' in {}; candidates: {}",
187                    symbol.name,
188                    file_path.display(),
189                    candidates
190                ))
191            }
192        }
193    }
194
195    pub fn symbols_in_file(&self, file_path: &Path) -> Result<Vec<SymbolMatch>, String> {
196        let ext = file_path
197            .extension()
198            .and_then(|e| e.to_str())
199            .ok_or_else(|| {
200                format!(
201                    "cannot determine language from file: {}",
202                    file_path.display()
203                )
204            })?;
205        let adapter = self
206            .registry
207            .get(ext)
208            .ok_or_else(|| format!("unsupported language extension: {ext}"))?;
209
210        let content = std::fs::read_to_string(file_path)
211            .map_err(|e| format!("failed to read {}: {e}", file_path.display()))?;
212        let mut parser = adapter.parser();
213        let tree = parser
214            .parse(&content, None)
215            .ok_or_else(|| format!("failed to parse {}", file_path.display()))?;
216        let mut symbols = Vec::new();
217        self.collect_symbols(tree.root_node(), &content, &mut symbols);
218        Ok(symbols)
219    }
220
221    pub fn is_import_only_reference(&self, file_path: &Path, line_number: usize) -> bool {
222        let ext = match file_path.extension().and_then(|e| e.to_str()) {
223            Some(ext) => ext,
224            None => return false,
225        };
226        let Some(adapter) = self.registry.get(ext) else {
227            return false;
228        };
229        if adapter.language_name() != "rust" {
230            return false;
231        }
232
233        let content = match std::fs::read_to_string(file_path) {
234            Ok(content) => content,
235            Err(_) => return false,
236        };
237        let mut parser = adapter.parser();
238        let Some(tree) = parser.parse(&content, None) else {
239            return false;
240        };
241
242        let query = match tree_sitter::Query::new(&adapter.language(), RUST_USE_IMPORT_QUERY) {
243            Ok(query) => query,
244            Err(_) => return false,
245        };
246        let mut cursor = tree_sitter::QueryCursor::new();
247        let mut matches = cursor.matches(&query, tree.root_node(), content.as_bytes());
248        while let Some(query_match) = matches.next() {
249            for capture in query_match.captures {
250                let node = capture.node;
251                let start = node.start_position().row + 1;
252                let end = node.end_position().row + 1;
253                if line_number >= start && line_number <= end {
254                    return true;
255                }
256            }
257        }
258
259        false
260    }
261
262    /// Validate that a file's content can be parsed by tree-sitter.
263    /// Returns true if parsing succeeds (i.e. the file is syntactically valid
264    /// for the given language), false otherwise.
265    pub fn can_parse(&self, ext: &str, content: &str) -> bool {
266        self.parse_error_diagnostic(ext, content).is_none()
267    }
268
269    /// Return the first tree-sitter parse diagnostic for this source, if any.
270    ///
271    /// Tree-sitter does not expose compiler-style syntax diagnostics. It
272    /// recovers by placing ERROR and MISSING nodes in the parse tree. This
273    /// helper reports the first such node with source coordinates and a compact
274    /// snippet so edit rejection messages can point at the likely problem.
275    pub fn parse_error_diagnostic(&self, ext: &str, content: &str) -> Option<ParseErrorDiagnostic> {
276        let adapter = match self.registry.get(ext) {
277            Some(adapter) => adapter,
278            None => {
279                return Some(ParseErrorDiagnostic {
280                    kind: ParseErrorKind::Error,
281                    node_kind: format!("unsupported extension `{ext}`"),
282                    start_line: 1,
283                    start_column: 1,
284                    end_line: 1,
285                    end_column: 1,
286                    snippet: parse_error_snippet(content, 1, 1),
287                });
288            }
289        };
290        let mut parser = adapter.parser();
291        let tree = parser.parse(content, None)?;
292        first_parse_error_node(tree.root_node()).map(|node| {
293            let start = node.start_position();
294            let end = node.end_position();
295            let start_line = start.row + 1;
296            let start_column = start.column + 1;
297            ParseErrorDiagnostic {
298                kind: if node.is_missing() {
299                    ParseErrorKind::Missing
300                } else {
301                    ParseErrorKind::Error
302                },
303                node_kind: node.kind().to_string(),
304                start_line,
305                start_column,
306                end_line: end.row + 1,
307                end_column: end.column + 1,
308                snippet: parse_error_snippet(content, start_line, start_column),
309            }
310        })
311    }
312
313    /// Return the SCOPE language adapter that owns semantic source operations
314    /// for the given extension.
315    pub fn responsible_language_for_extension(&self, ext: &str) -> Option<&'static str> {
316        self.registry
317            .get(ext)
318            .map(|adapter| adapter.language_name())
319    }
320
321    /// Return true when SCOPE owns semantic source operations for this path.
322    pub fn is_responsible_source_path(&self, file_path: &Path) -> bool {
323        file_path
324            .extension()
325            .and_then(|ext| ext.to_str())
326            .and_then(|ext| self.responsible_language_for_extension(ext))
327            .is_some()
328    }
329
330    fn collect_symbols(
331        &self,
332        node: tree_sitter::Node,
333        source: &str,
334        symbols: &mut Vec<SymbolMatch>,
335    ) {
336        let kind = node.kind();
337        if is_definition_kind(kind)
338            && let Some(name) = self.extract_def_name(node, source)
339        {
340            let start_line = node.start_position().row + 1;
341            let end_line = node.end_position().row + 1;
342            symbols.push(SymbolMatch {
343                name,
344                kind: SymbolKind::from_ts_node_kind(kind),
345                kind_prefix: kind_prefix(kind),
346                start_line,
347                end_line,
348            });
349        }
350
351        for i in 0..node.child_count() {
352            if let Some(child) = node.child(i) {
353                self.collect_symbols(child, source, symbols);
354            }
355        }
356    }
357
358    fn extract_def_name(&self, node: tree_sitter::Node, source: &str) -> Option<String> {
359        for i in 0..node.child_count() {
360            let child = node.child(i)?;
361            let kind = child.kind();
362            if kind == "identifier" || kind == "type_identifier" {
363                return child
364                    .utf8_text(source.as_bytes())
365                    .ok()
366                    .map(|s| s.to_string());
367            }
368        }
369        None
370    }
371}
372
373fn symbol_matches_selector(symbol: &SymbolMatch, parsed: &ParsedSelector) -> bool {
374    let Some(selector) = parsed.as_symbol() else {
375        return false;
376    };
377    symbol_matches_symbol_selector(symbol, selector)
378}
379
380fn symbol_matches_symbol_selector(symbol: &SymbolMatch, selector: &SymbolSelector) -> bool {
381    symbol.name == selector.name
382        && (selector.kind == SymbolKind::Unknown || symbol.kind == selector.kind)
383}
384
385fn is_definition_kind(kind: &str) -> bool {
386    matches!(
387        kind,
388        "function_item"
389            | "struct_item"
390            | "enum_item"
391            | "trait_item"
392            | "impl_item"
393            | "function_definition"
394            | "class_definition"
395            | "decorated_definition"
396            | "function_declaration"
397            | "class_declaration"
398            | "interface_declaration"
399            | "enum_declaration"
400            | "method_definition"
401            | "type_alias_declaration"
402    )
403}
404
405fn kind_prefix(kind: &str) -> &'static str {
406    match kind {
407        "function_item" => "fn ",
408        "struct_item" => "struct ",
409        "enum_item" => "enum ",
410        "trait_item" => "trait ",
411        "impl_item" => "impl ",
412        "function_definition" => "fn ",
413        "class_definition" => "class ",
414        "function_declaration" => "fn ",
415        "class_declaration" => "class ",
416        "interface_declaration" => "trait ",
417        "enum_declaration" => "enum ",
418        "method_definition" => "fn ",
419        "type_alias_declaration" => "type ",
420        _ => "",
421    }
422}
423
424fn first_parse_error_node<'tree>(
425    node: tree_sitter::Node<'tree>,
426) -> Option<tree_sitter::Node<'tree>> {
427    if node.is_error() || node.is_missing() {
428        return Some(node);
429    }
430
431    let mut cursor = node.walk();
432    for child in node.children(&mut cursor) {
433        if (child.has_error() || child.is_error() || child.is_missing())
434            && let Some(found) = first_parse_error_node(child)
435        {
436            return Some(found);
437        }
438    }
439    None
440}
441
442fn parse_error_snippet(content: &str, line: usize, column: usize) -> String {
443    let lines = content.lines().collect::<Vec<_>>();
444    let start_line = line.saturating_sub(2).max(1);
445    let end_line = line
446        .saturating_add(2)
447        .max(start_line)
448        .min(lines.len().max(line));
449    let width = end_line.to_string().len().max(1);
450    let mut snippet = String::new();
451
452    for current_line in start_line..=end_line {
453        let text = lines
454            .get(current_line.saturating_sub(1))
455            .copied()
456            .unwrap_or("");
457        snippet.push_str(&format!("{current_line:>width$} | {text}\n"));
458        if current_line == line {
459            let caret_padding = " ".repeat(column.saturating_sub(1));
460            snippet.push_str(&format!("{:>width$} | {caret_padding}^\n", ""));
461        }
462    }
463
464    snippet.trim_end().to_string()
465}
466
467const RUST_USE_IMPORT_QUERY: &str = "(use_declaration) @import";
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use std::io::Write;
473    use std::path::PathBuf;
474
475    fn write_temp_rust_file(dir: &Path, name: &str, content: &str) -> PathBuf {
476        let path = dir.join(name);
477        let mut f = std::fs::File::create(&path).unwrap();
478        f.write_all(content.as_bytes()).unwrap();
479        path
480    }
481
482    const RUST_CODE: &str = "// line 1\n                 fn startup() {\n                    inner_call();\n                }\n            }\n            ";
483
484    #[test]
485    fn test_find_containing_symbol_fn() {
486        let dir = tempfile::tempdir().unwrap();
487        let path = write_temp_rust_file(dir.path(), "test.rs", RUST_CODE);
488        let analyzer = TreeSitterAnalyzer::new();
489        // Line 4 should be inside startup() (adjusted for the actual structure)
490        let result = analyzer.find_containing_symbol(&path, 3, dir.path());
491        // Just check it doesn't crash; exact line numbers depend on the test string
492        println!("find_containing_symbol result: {:?}", result);
493    }
494
495    #[test]
496    fn symbol_match_source_from_returns_exact_line_range() {
497        let symbol = SymbolMatch {
498            name: "target".to_string(),
499            kind: SymbolKind::Function,
500            kind_prefix: "fn ",
501            start_line: 3,
502            end_line: 5,
503        };
504        let content = "line 1\nline 2\nfn target() {\n    body();\n}\nfn other() {}\n";
505        assert_eq!(
506            symbol.source_from(content),
507            "fn target() {\n    body();\n}\n"
508        );
509    }
510
511    #[test]
512    fn tsx_files_use_tsx_parser_and_expose_top_level_functions() {
513        let dir = tempfile::tempdir().unwrap();
514        let path = dir.path().join("status-page.tsx");
515        std::fs::write(
516            &path,
517            "function AgentChatActivityHeader() {\n  return <div />;\n}\n\nfunction agentChatActivityGlyph(bubble: { kind: string }) {\n  return bubble.kind;\n}\n",
518        )
519        .unwrap();
520        let analyzer = TreeSitterAnalyzer::new();
521
522        assert!(analyzer.can_parse("tsx", &std::fs::read_to_string(&path).unwrap()));
523        let symbol = analyzer
524            .resolve_selector(
525                &path,
526                &crate::selector::parse_selector("status-page.tsx::fn agentChatActivityGlyph")
527                    .unwrap(),
528            )
529            .expect("TSX top-level function should resolve");
530        assert_eq!(symbol.name, "agentChatActivityGlyph");
531        assert_eq!(symbol.start_line, 5);
532    }
533
534    #[test]
535    fn canonical_selector_disambiguates_duplicate_method_names() {
536        let dir = tempfile::tempdir().unwrap();
537        let code = r#"trait Hints {
538    fn setup_hints(&self);
539}
540
541struct Alpha;
542struct Beta;
543
544impl Hints for Alpha {
545    fn setup_hints(&self) {
546        println!("alpha");
547    }
548}
549
550impl Hints for Beta {
551    fn setup_hints(&self) {
552        println!("beta");
553    }
554}
555"#;
556        let path = write_temp_rust_file(dir.path(), "dup.rs", code);
557        let analyzer = TreeSitterAnalyzer::new();
558
559        let canonical = analyzer
560            .find_containing_symbol(&path, 16, dir.path())
561            .expect("line inside Beta::setup_hints should resolve");
562        assert!(canonical.starts_with("dup.rs::fn setup_hints #L"));
563        assert!(canonical.contains("-L"));
564
565        let parsed = crate::selector::parse_selector(&canonical).unwrap();
566        let resolved = analyzer.resolve_selector(&path, &parsed).unwrap();
567        assert_eq!(resolved.name, "setup_hints");
568        assert_eq!(resolved.start_line, 15);
569    }
570
571    #[test]
572    fn legacy_duplicate_method_selector_is_rejected_as_ambiguous() {
573        let dir = tempfile::tempdir().unwrap();
574        let code = r#"trait Hints {
575    fn setup_hints(&self);
576}
577
578struct Alpha;
579struct Beta;
580
581impl Hints for Alpha {
582    fn setup_hints(&self) {}
583}
584
585impl Hints for Beta {
586    fn setup_hints(&self) {}
587}
588"#;
589        let path = write_temp_rust_file(dir.path(), "dup.rs", code);
590        let analyzer = TreeSitterAnalyzer::new();
591        let parsed = crate::selector::parse_selector("dup.rs::fn setup_hints").unwrap();
592        let err = analyzer.resolve_selector(&path, &parsed).unwrap_err();
593        assert!(err.contains("ambiguous selector"));
594        assert!(err.contains("#L"));
595    }
596
597    #[test]
598    fn test_can_parse_valid_rust() {
599        let analyzer = TreeSitterAnalyzer::new();
600        let valid = "fn main() { println!(\"hello\"); }";
601        assert!(analyzer.can_parse("rs", valid));
602    }
603
604    #[test]
605    fn rust_use_declaration_is_import_only_reference() {
606        let dir = tempfile::tempdir().unwrap();
607        let code = r#"use crate::parser::Parser;
608use crate::{engine::Engine, runtime};
609
610fn run() {
611    Parser::new();
612}
613"#;
614        let path = write_temp_rust_file(dir.path(), "imports.rs", code);
615        let analyzer = TreeSitterAnalyzer::new();
616
617        assert!(analyzer.is_import_only_reference(&path, 1));
618        assert!(analyzer.is_import_only_reference(&path, 2));
619        assert!(!analyzer.is_import_only_reference(&path, 5));
620    }
621
622    #[test]
623    fn test_can_parse_rejects_rust_error_nodes() {
624        let analyzer = TreeSitterAnalyzer::new();
625        let invalid = "fn main( {\n";
626        assert!(!analyzer.can_parse("rs", invalid));
627    }
628
629    #[test]
630    fn parse_error_diagnostic_reports_location_and_snippet() {
631        let analyzer = TreeSitterAnalyzer::new();
632        let invalid = "fn main( {\n";
633
634        let diagnostic = analyzer
635            .parse_error_diagnostic("rs", invalid)
636            .expect("invalid rust should produce a parse diagnostic");
637        let message = diagnostic.message();
638
639        assert!(message.contains("first parse error:"));
640        assert!(message.contains("L1:C"));
641        assert!(message.contains("fn main( {"));
642        assert!(message.contains('^'));
643    }
644
645    #[test]
646    fn test_can_parse_empty_string() {
647        let analyzer = TreeSitterAnalyzer::new();
648        assert!(analyzer.can_parse("rs", ""));
649    }
650
651    #[test]
652    fn test_can_parse_unknown_language_returns_false() {
653        let analyzer = TreeSitterAnalyzer::new();
654        assert!(!analyzer.can_parse("unknown_ext", "fn main() {}"));
655    }
656
657    #[test]
658    fn test_can_parse_valid_python() {
659        let analyzer = TreeSitterAnalyzer::new();
660        let py_code = "def greet(name):\n    return f\"Hello, {name}!\"\n";
661        assert!(analyzer.can_parse("py", py_code));
662    }
663
664    #[test]
665    fn test_can_parse_valid_go() {
666        let analyzer = TreeSitterAnalyzer::new();
667        let go_code = "package main\nfunc greet(name string) string { return \"Hello\" }\n";
668        assert!(analyzer.can_parse("go", go_code));
669    }
670
671    #[test]
672    fn test_can_parse_valid_java() {
673        let analyzer = TreeSitterAnalyzer::new();
674        let java_code = "public class Hello { public static void main(String[] args) {} }\n";
675        assert!(analyzer.can_parse("java", java_code));
676    }
677
678    #[test]
679    fn test_can_parse_valid_typescript() {
680        let analyzer = TreeSitterAnalyzer::new();
681        let ts_code = "function greet(name: string): string { return \"Hello\"; }\n";
682        assert!(analyzer.can_parse("ts", ts_code));
683    }
684
685    #[test]
686    fn test_can_parse_valid_javascript() {
687        let analyzer = TreeSitterAnalyzer::new();
688        let js_code = "function greet(name) { return \"Hello\"; }\n";
689        assert!(analyzer.can_parse("js", js_code));
690    }
691
692    #[test]
693    fn test_can_parse_valid_c() {
694        let analyzer = TreeSitterAnalyzer::new();
695        let c_code = "int main() { return 0; }\n";
696        assert!(analyzer.can_parse("c", c_code));
697    }
698
699    #[test]
700    fn test_can_parse_valid_cpp() {
701        let analyzer = TreeSitterAnalyzer::new();
702        let cpp_code = "class Hello { public: void greet() {} };\n";
703        assert!(analyzer.can_parse("cpp", cpp_code));
704    }
705
706    #[test]
707    fn test_can_parse_valid_ruby() {
708        let analyzer = TreeSitterAnalyzer::new();
709        let ruby_code = "def greet(name)\n  \"Hello, #{name}!\"\nend\n";
710        assert!(analyzer.can_parse("rb", ruby_code));
711    }
712
713    #[test]
714    fn test_can_parse_valid_php() {
715        let analyzer = TreeSitterAnalyzer::new();
716        let php_code = "<?php\nfunction greet($name) { return \"Hello\"; }\n";
717        assert!(analyzer.can_parse("php", php_code));
718    }
719
720    #[test]
721    fn responsible_source_path_matches_registered_languages() {
722        let analyzer = TreeSitterAnalyzer::new();
723        assert!(analyzer.is_responsible_source_path(Path::new("src/lib.rs")));
724        assert!(analyzer.is_responsible_source_path(Path::new("script.py")));
725        assert!(!analyzer.is_responsible_source_path(Path::new("README.md")));
726        assert!(!analyzer.is_responsible_source_path(Path::new("Makefile")));
727        assert_eq!(
728            analyzer.responsible_language_for_extension("rs"),
729            Some("rust")
730        );
731        assert_eq!(analyzer.responsible_language_for_extension("md"), None);
732    }
733    #[test]
734    fn test_language_registry_has_all_languages() {
735        let registry = LanguageRegistry::new();
736        assert!(registry.get("rs").is_some(), "Rust should be registered");
737        assert!(registry.get("py").is_some(), "Python should be registered");
738        assert!(registry.get("go").is_some(), "Go should be registered");
739        assert!(registry.get("java").is_some(), "Java should be registered");
740        assert!(
741            registry.get("ts").is_some(),
742            "TypeScript should be registered"
743        );
744        assert!(
745            registry.get("js").is_some(),
746            "JavaScript should be registered"
747        );
748        assert!(registry.get("c").is_some(), "C should be registered");
749        assert!(registry.get("cpp").is_some(), "C++ should be registered");
750        assert!(registry.get("rb").is_some(), "Ruby should be registered");
751        assert!(registry.get("php").is_some(), "PHP should be registered");
752    }
753
754    #[test]
755    fn test_language_registry_all_names() {
756        let registry = LanguageRegistry::new();
757        let langs = registry.list_languages();
758        let names: Vec<&str> = langs.iter().map(|(n, _)| *n).collect();
759        assert!(names.contains(&"rust"), "rust in {:?}", names);
760        assert!(names.contains(&"python"), "python in {:?}", names);
761        assert!(names.contains(&"go"), "go in {:?}", names);
762        assert!(names.contains(&"java"), "java in {:?}", names);
763        assert!(names.contains(&"typescript"), "typescript in {:?}", names);
764        assert!(names.contains(&"javascript"), "javascript in {:?}", names);
765        assert!(names.contains(&"c"), "c in {:?}", names);
766        assert!(names.contains(&"cpp"), "cpp in {:?}", names);
767        assert!(names.contains(&"ruby"), "ruby in {:?}", names);
768        assert!(names.contains(&"php"), "php in {:?}", names);
769    }
770}