use tree_sitter::{Language, Parser, TreeCursor}; #[derive(Debug, Clone)] pub struct ParsedSymbol { pub name: String, pub kind: String, pub line: u32, pub end_line: u32, pub is_definition: bool, } pub fn language_for_path(path: &str) -> Option<(&'static str, Language)> { let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); let extension = name.rsplit_once('.').map(|(_, ext)| ext)?; let language = match extension { "rs" => ("Rust", tree_sitter_rust::LANGUAGE.into()), "js" | "mjs" | "cjs" | "jsx" => ("JavaScript", tree_sitter_javascript::LANGUAGE.into()), "ts" => ( "TypeScript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), ), "tsx" => ("TypeScript", tree_sitter_typescript::LANGUAGE_TSX.into()), "py" => ("Python", tree_sitter_python::LANGUAGE.into()), "go" => ("Go", tree_sitter_go::LANGUAGE.into()), "java" => ("Java", tree_sitter_java::LANGUAGE.into()), "c" | "h" => ("C", tree_sitter_c::LANGUAGE.into()), "cc" | "cp" | "cpp" | "cxx" | "c++" | "hh" | "hpp" | "hxx" => { ("C++", tree_sitter_cpp::LANGUAGE.into()) } _ => return None, }; Some(language) } pub fn symbols_for(path: &str, text: &str) -> Vec { let Some((_, language)) = language_for_path(path) else { return Vec::new(); }; let mut parser = Parser::new(); if parser.set_language(&language).is_err() { return Vec::new(); } let Some(tree) = parser.parse(text, None) else { return Vec::new(); }; let bytes = text.as_bytes(); let mut out = Vec::new(); let mut cursor = tree.root_node().walk(); visit(&mut cursor, bytes, &mut out); out } fn visit(cursor: &mut TreeCursor<'_>, source: &[u8], out: &mut Vec) { let node = cursor.node(); let kind = node.kind(); if is_definition_kind(kind) { if let Some(name) = declaration_name(node, source) { out.push(ParsedSymbol { name, kind: kind.to_string(), line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, is_definition: true, }); } } else if is_identifier_kind(kind) { if let Ok(name) = node.utf8_text(source) { if !name.is_empty() { out.push(ParsedSymbol { name: name.to_string(), kind: "reference".to_string(), line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, is_definition: false, }); } } } if cursor.goto_first_child() { loop { visit(cursor, source, out); if !cursor.goto_next_sibling() { break; } } cursor.goto_parent(); } } fn declaration_name(node: tree_sitter::Node<'_>, source: &[u8]) -> Option { for field in ["name", "declarator", "type", "field"] { if let Some(child) = node.child_by_field_name(field) { if is_identifier_kind(child.kind()) { return child.utf8_text(source).ok().map(ToOwned::to_owned); } if let Some(name) = first_identifier(child, source) { return Some(name); } } } first_identifier(node, source) } fn first_identifier(node: tree_sitter::Node<'_>, source: &[u8]) -> Option { if is_identifier_kind(node.kind()) { return node.utf8_text(source).ok().map(ToOwned::to_owned); } let mut cursor = node.walk(); if cursor.goto_first_child() { loop { if let Some(name) = first_identifier(cursor.node(), source) { return Some(name); } if !cursor.goto_next_sibling() { break; } } } None } fn is_identifier_kind(kind: &str) -> bool { matches!( kind, "identifier" | "field_identifier" | "type_identifier" | "namespace_identifier" ) } fn is_definition_kind(kind: &str) -> bool { matches!( kind, "function_item" | "function_declaration" | "function_definition" | "method_definition" | "method_declaration" | "class_definition" | "class_declaration" | "interface_declaration" | "struct_item" | "struct_specifier" | "enum_item" | "enum_specifier" | "trait_item" | "impl_item" | "type_item" | "type_declaration" | "module" | "namespace_definition" ) }