Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! Symbol extraction from source files.
Matt W2//!
Matt W3//! Extracts top-level symbols (functions, structs, classes, interfaces, etc.)
Matt W4//! from source code using tree-sitter grammars. This powers the symbol outline
Matt W5//! sidebar in the code view.
Matt W6//!
Matt W7//! The extraction is best-effort: languages without a grammar or files that fail
Matt W8//! to parse simply return an empty list, never an error.
Matt W9
Matt W10use tree_sitter::{Parser, Tree};
Matt W11
Matt W12/// A symbol extracted from a source file.
Matt W13#[derive(Debug, Clone, PartialEq, Eq)]
Matt W14pub struct Symbol {
Matt W15 /// The symbol name (e.g. `main`, `MyStruct`, `handle_request`).
Matt W16 pub name: String,
Matt W17 /// What kind of symbol this is.
Matt W18 pub kind: SymbolKind,
Matt W19 /// 1-based line number where the symbol starts.
Matt W20 pub line: usize,
Matt W21}
Matt W22
Matt W23/// The kind of symbol.
Matt W24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W25pub enum SymbolKind {
Matt W26 Function,
Matt W27 Method,
Matt W28 Class,
Matt W29 Struct,
Matt W30 Enum,
Matt W31 Interface,
Matt W32 Trait,
Matt W33 Constant,
Matt W34 Module,
Matt W35 Type,
Matt W36}
Matt W37
Matt W38impl SymbolKind {
Matt W39 /// Short label for the UI.
Matt W40 pub fn label(&self) -> &'static str {
Matt W41 match self {
Matt W42 SymbolKind::Function => "fn",
Matt W43 SymbolKind::Method => "method",
Matt W44 SymbolKind::Class => "class",
Matt W45 SymbolKind::Struct => "struct",
Matt W46 SymbolKind::Enum => "enum",
Matt W47 SymbolKind::Interface => "iface",
Matt W48 SymbolKind::Trait => "trait",
Matt W49 SymbolKind::Constant => "const",
Matt W50 SymbolKind::Module => "mod",
Matt W51 SymbolKind::Type => "type",
Matt W52 }
Matt W53 }
Matt W54
Matt W55 /// CSS class suffix for styling.
Matt W56 pub fn css_class(&self) -> &'static str {
Matt W57 match self {
Matt W58 SymbolKind::Function | SymbolKind::Method => "fn",
Matt W59 SymbolKind::Class | SymbolKind::Struct => "type",
Matt W60 SymbolKind::Enum => "enum",
Matt W61 SymbolKind::Interface | SymbolKind::Trait => "trait",
Matt W62 SymbolKind::Constant => "const",
Matt W63 SymbolKind::Module => "mod",
Matt W64 SymbolKind::Type => "type",
Matt W65 }
Matt W66 }
Matt W67}
Matt W68
Matt W69/// Extract symbols from the given source code.
Matt W70///
Matt W71/// `path` is used to determine the language (by extension). Returns an empty
Matt W72/// vec for unsupported languages or if parsing fails.
Matt W73pub fn extract_symbols(path: &str, source: &str) -> Vec<Symbol> {
Matt W74 // Don't attempt symbol extraction on very large files.
Matt W75 if source.len() > 1024 * 1024 {
Matt W76 return Vec::new();
Matt W77 }
Matt W78
Matt W79 let lang = match language_for_symbols(path) {
Matt W80 Some(l) => l,
Matt W81 None => return Vec::new(),
Matt W82 };
Matt W83
Matt W84 let mut parser = Parser::new();
Matt W85 if parser.set_language(&lang.ts_language).is_err() {
Matt W86 return Vec::new();
Matt W87 }
Matt W88
Matt W89 let tree = match parser.parse(source, None) {
Matt W90 Some(t) => t,
Matt W91 None => return Vec::new(),
Matt W92 };
Matt W93
Matt W94 extract_from_tree(&tree, source, lang.node_kinds)
Matt W95}
Matt W96
Matt W97/// Walk the tree and collect symbols based on language-specific node kinds.
Matt W98fn extract_from_tree(tree: &Tree, source: &str, node_kinds: &[NodeKindMapping]) -> Vec<Symbol> {
Matt W99 let mut symbols = Vec::new();
Matt W100 let root = tree.root_node();
Matt W101
Matt W102 let mut cursor = root.walk();
Matt W103 // Only walk top-level and one level deep (for impl blocks, class bodies).
Matt W104 for node in root.children(&mut cursor) {
Matt W105 extract_node(&node, source, node_kinds, &mut symbols, 0);
Matt W106 }
Matt W107
Matt W108 symbols
Matt W109}
Matt W110
Matt W111/// Maximum nesting depth for symbol extraction.
Matt W112const MAX_DEPTH: usize = 2;
Matt W113
Matt W114fn extract_node(
Matt W115 node: &tree_sitter::Node,
Matt W116 source: &str,
Matt W117 node_kinds: &[NodeKindMapping],
Matt W118 symbols: &mut Vec<Symbol>,
Matt W119 depth: usize,
Matt W120) {
Matt W121 let kind = node.kind();
Matt W122
Matt W123 // Check if this node kind maps to a symbol.
Matt W124 for mapping in node_kinds {
Matt W125 if mapping.node_kind == kind {
Matt W126 if let Some(name) = find_name_child(node, source, mapping.name_field) {
Matt W127 symbols.push(Symbol {
Matt W128 name,
Matt W129 kind: mapping.symbol_kind,
Matt W130 line: node.start_position().row + 1,
Matt W131 });
Matt W132 }
Matt W133 break;
Matt W134 }
Matt W135 }
Matt W136
Matt W137 // Recurse into container nodes (impl blocks, class bodies) but not too deep.
Matt W138 if depth < MAX_DEPTH && is_container_node(kind) {
Matt W139 let mut cursor = node.walk();
Matt W140 for child in node.children(&mut cursor) {
Matt W141 extract_node(&child, source, node_kinds, symbols, depth + 1);
Matt W142 }
Matt W143 }
Matt W144}
Matt W145
Matt W146/// Find the name of a node by looking for a specific child field.
Matt W147fn find_name_child(
Matt W148 node: &tree_sitter::Node,
Matt W149 source: &str,
Matt W150 name_field: &str,
Matt W151) -> Option<String> {
Matt W152 let child = node.child_by_field_name(name_field)?;
Matt W153 let text = child.utf8_text(source.as_bytes()).ok()?;
Matt W154 // Limit name length to avoid absurd entries.
Matt W155 if text.len() > 200 {
Matt W156 return None;
Matt W157 }
Matt W158 Some(text.to_string())
Matt W159}
Matt W160
Matt W161/// Whether a node is a "container" that might have nested symbols.
Matt W162fn is_container_node(kind: &str) -> bool {
Matt W163 matches!(
Matt W164 kind,
Matt W165 "impl_item"
Matt W166 | "class_declaration"
Matt W167 | "class_definition"
Matt W168 | "class_body"
Matt W169 | "module"
Matt W170 | "namespace_declaration"
Matt W171 | "object_declaration"
Matt W172 | "trait_definition"
Matt W173 | "interface_declaration"
Matt W174 | "declaration_list"
Matt W175 | "block"
Matt W176 | "program"
Matt W177 )
Matt W178}
Matt W179
Matt W180// ─── Language-specific configuration ─────────────────────────────────────────
Matt W181
Matt W182struct NodeKindMapping {
Matt W183 node_kind: &'static str,
Matt W184 name_field: &'static str,
Matt W185 symbol_kind: SymbolKind,
Matt W186}
Matt W187
Matt W188struct SymbolLanguage {
Matt W189 ts_language: tree_sitter::Language,
Matt W190 node_kinds: &'static [NodeKindMapping],
Matt W191}
Matt W192
Matt W193const RUST_KINDS: &[NodeKindMapping] = &[
Matt W194 NodeKindMapping { node_kind: "function_item", name_field: "name", symbol_kind: SymbolKind::Function },
Matt W195 NodeKindMapping { node_kind: "struct_item", name_field: "name", symbol_kind: SymbolKind::Struct },
Matt W196 NodeKindMapping { node_kind: "enum_item", name_field: "name", symbol_kind: SymbolKind::Enum },
Matt W197 NodeKindMapping { node_kind: "trait_item", name_field: "name", symbol_kind: SymbolKind::Trait },
Matt W198 NodeKindMapping { node_kind: "type_item", name_field: "name", symbol_kind: SymbolKind::Type },
Matt W199 NodeKindMapping { node_kind: "const_item", name_field: "name", symbol_kind: SymbolKind::Constant },
Matt W200 NodeKindMapping { node_kind: "mod_item", name_field: "name", symbol_kind: SymbolKind::Module },
Matt W201 NodeKindMapping { node_kind: "impl_item", name_field: "type", symbol_kind: SymbolKind::Struct },
Matt W202];
Matt W203
Matt W204const PYTHON_KINDS: &[NodeKindMapping] = &[
Matt W205 NodeKindMapping { node_kind: "function_definition", name_field: "name", symbol_kind: SymbolKind::Function },
Matt W206 NodeKindMapping { node_kind: "class_definition", name_field: "name", symbol_kind: SymbolKind::Class },
Matt W207];
Matt W208
Matt W209const JS_KINDS: &[NodeKindMapping] = &[
Matt W210 NodeKindMapping { node_kind: "function_declaration", name_field: "name", symbol_kind: SymbolKind::Function },
Matt W211 NodeKindMapping { node_kind: "class_declaration", name_field: "name", symbol_kind: SymbolKind::Class },
Matt W212 NodeKindMapping { node_kind: "method_definition", name_field: "name", symbol_kind: SymbolKind::Method },
Matt W213];
Matt W214
Matt W215const GO_KINDS: &[NodeKindMapping] = &[
Matt W216 NodeKindMapping { node_kind: "function_declaration", name_field: "name", symbol_kind: SymbolKind::Function },
Matt W217 NodeKindMapping { node_kind: "method_declaration", name_field: "name", symbol_kind: SymbolKind::Method },
Matt W218 NodeKindMapping { node_kind: "type_declaration", name_field: "name", symbol_kind: SymbolKind::Type },
Matt W219];
Matt W220
Matt W221const JAVA_KINDS: &[NodeKindMapping] = &[
Matt W222 NodeKindMapping { node_kind: "class_declaration", name_field: "name", symbol_kind: SymbolKind::Class },
Matt W223 NodeKindMapping { node_kind: "method_declaration", name_field: "name", symbol_kind: SymbolKind::Method },
Matt W224 NodeKindMapping { node_kind: "interface_declaration", name_field: "name", symbol_kind: SymbolKind::Interface },
Matt W225 NodeKindMapping { node_kind: "enum_declaration", name_field: "name", symbol_kind: SymbolKind::Enum },
Matt W226];
Matt W227
Matt W228const C_KINDS: &[NodeKindMapping] = &[
Matt W229 NodeKindMapping { node_kind: "function_definition", name_field: "declarator", symbol_kind: SymbolKind::Function },
Matt W230 NodeKindMapping { node_kind: "struct_specifier", name_field: "name", symbol_kind: SymbolKind::Struct },
Matt W231 NodeKindMapping { node_kind: "enum_specifier", name_field: "name", symbol_kind: SymbolKind::Enum },
Matt W232];
Matt W233
Matt W234const RUBY_KINDS: &[NodeKindMapping] = &[
Matt W235 NodeKindMapping { node_kind: "method", name_field: "name", symbol_kind: SymbolKind::Method },
Matt W236 NodeKindMapping { node_kind: "class", name_field: "name", symbol_kind: SymbolKind::Class },
Matt W237 NodeKindMapping { node_kind: "module", name_field: "name", symbol_kind: SymbolKind::Module },
Matt W238];
Matt W239
Matt W240fn language_for_symbols(path: &str) -> Option<SymbolLanguage> {
Matt W241 let name = path.rsplit('/').next().unwrap_or(path);
Matt W242 let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase())?;
Matt W243
Matt W244 match ext.as_str() {
Matt W245 "rs" => Some(SymbolLanguage {
Matt W246 ts_language: tree_sitter_rust::LANGUAGE.into(),
Matt W247 node_kinds: RUST_KINDS,
Matt W248 }),
Matt W249 "py" | "pyi" | "pyw" => Some(SymbolLanguage {
Matt W250 ts_language: tree_sitter_python::LANGUAGE.into(),
Matt W251 node_kinds: PYTHON_KINDS,
Matt W252 }),
Matt W253 "js" | "mjs" | "cjs" | "jsx" => Some(SymbolLanguage {
Matt W254 ts_language: tree_sitter_javascript::LANGUAGE.into(),
Matt W255 node_kinds: JS_KINDS,
Matt W256 }),
Matt W257 "ts" | "mts" | "cts" => Some(SymbolLanguage {
Matt W258 ts_language: tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
Matt W259 node_kinds: JS_KINDS,
Matt W260 }),
Matt W261 "tsx" => Some(SymbolLanguage {
Matt W262 ts_language: tree_sitter_typescript::LANGUAGE_TSX.into(),
Matt W263 node_kinds: JS_KINDS,
Matt W264 }),
Matt W265 "go" => Some(SymbolLanguage {
Matt W266 ts_language: tree_sitter_go::LANGUAGE.into(),
Matt W267 node_kinds: GO_KINDS,
Matt W268 }),
Matt W269 "c" | "h" => Some(SymbolLanguage {
Matt W270 ts_language: tree_sitter_c::LANGUAGE.into(),
Matt W271 node_kinds: C_KINDS,
Matt W272 }),
Matt W273 "cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => Some(SymbolLanguage {
Matt W274 ts_language: tree_sitter_cpp::LANGUAGE.into(),
Matt W275 node_kinds: C_KINDS,
Matt W276 }),
Matt W277 "java" => Some(SymbolLanguage {
Matt W278 ts_language: tree_sitter_java::LANGUAGE.into(),
Matt W279 node_kinds: JAVA_KINDS,
Matt W280 }),
Matt W281 "rb" | "rake" | "gemspec" => Some(SymbolLanguage {
Matt W282 ts_language: tree_sitter_ruby::LANGUAGE.into(),
Matt W283 node_kinds: RUBY_KINDS,
Matt W284 }),
Matt W285 _ => None,
Matt W286 }
Matt W287}
Matt W288
Matt W289#[cfg(test)]
Matt W290mod tests {
Matt W291 use super::*;
Matt W292
Matt W293 #[test]
Matt W294 fn extracts_rust_symbols() {
Matt W295 let source = r#"
Matt W296fn main() {
Matt W297 println!("hello");
Matt W298}
Matt W299
Matt W300struct Foo {
Matt W301 x: i32,
Matt W302}
Matt W303
Matt W304enum Bar {
Matt W305 A,
Matt W306 B,
Matt W307}
Matt W308
Matt W309trait Baz {
Matt W310 fn method(&self);
Matt W311}
Matt W312"#;
Matt W313 let symbols = extract_symbols("src/main.rs", source);
Matt W314 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
Matt W315 assert!(names.contains(&"main"));
Matt W316 assert!(names.contains(&"Foo"));
Matt W317 assert!(names.contains(&"Bar"));
Matt W318 assert!(names.contains(&"Baz"));
Matt W319 }
Matt W320
Matt W321 #[test]
Matt W322 fn extracts_python_symbols() {
Matt W323 let source = r#"
Matt W324def hello():
Matt W325 pass
Matt W326
Matt W327class World:
Matt W328 def method(self):
Matt W329 pass
Matt W330"#;
Matt W331 let symbols = extract_symbols("app.py", source);
Matt W332 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
Matt W333 assert!(names.contains(&"hello"));
Matt W334 assert!(names.contains(&"World"));
Matt W335 }
Matt W336
Matt W337 #[test]
Matt W338 fn unsupported_language_returns_empty() {
Matt W339 let symbols = extract_symbols("data.csv", "a,b,c\n1,2,3\n");
Matt W340 assert!(symbols.is_empty());
Matt W341 }
Matt W342
Matt W343 #[test]
Matt W344 fn empty_source_returns_empty() {
Matt W345 let symbols = extract_symbols("main.rs", "");
Matt W346 assert!(symbols.is_empty());
Matt W347 }
Matt W348}

348 lines · Rust