use std::cell::RefCell; use tree_sitter::{Language, Parser, TreeCursor}; thread_local! { static PARSER: RefCell> = const { RefCell::new(None) }; } #[derive(Debug, Clone)] pub struct ParsedSymbol { pub name: String, pub kind: String, pub line: u32, pub end_line: u32, pub is_definition: bool, } fn file_name(path: &str) -> String { path.rsplit(['/', '\\']) .next() .unwrap_or(path) .to_ascii_lowercase() } /// 锁文件、压缩产物、sourcemap 参与检索只会制造噪音。 fn is_generated_file(name: &str) -> bool { matches!( name, "package-lock.json" | "npm-shrinkwrap.json" | "pnpm-lock.yaml" | "yarn.lock" | "composer.lock" | "podfile.lock" | "gemfile.lock" ) || name.ends_with(".min.js") || name.ends_with(".min.css") || name.ends_with(".map") || name.ends_with(".d.ts") } /// 代码检索覆盖的语言:按文件名给出展示与过滤用的语言名。 /// 未收录的后缀视为不可索引,避免把二进制或生成产物灌进索引。 pub fn language_label(path: &str) -> Option<&'static str> { let name = file_name(path); if is_generated_file(&name) { return None; } if let Some((_, extension)) = name.rsplit_once('.') { return Some(match extension { "rs" => "Rust", "js" | "mjs" | "cjs" | "jsx" | "flow" => "JavaScript", "ts" => "TypeScript", "tsx" => "TypeScript (React)", "py" | "pyi" | "pyw" => "Python", "go" => "Go", "java" => "Java", "kt" | "kts" => "Kotlin", "swift" => "Swift", "scala" | "sc" => "Scala", "c" | "h" => "C", "cc" | "cp" | "cpp" | "cxx" | "c++" | "hh" | "hpp" | "hxx" => "C++", "cs" | "csx" => "C#", "rb" | "rake" | "gemspec" => "Ruby", "php" | "phtml" => "PHP", "pl" | "pm" => "Perl", "lua" => "Lua", "r" => "R", "m" => "Objective-C", "mm" => "Objective-C++", "sh" | "bash" | "zsh" | "ksh" | "cli" => "Shell", "fish" => "fish", "ps1" | "psm1" | "psd1" => "PowerShell", "bat" | "cmd" => "Batch", "sql" => "SQL", "html" | "htm" | "xhtml" => "HTML", "vue" => "Vue", "svelte" => "Svelte", "astro" => "Astro", "css" => "CSS", "scss" => "SCSS", "sass" => "Sass", "less" => "Less", "styl" => "Stylus", "xml" | "xsd" | "xsl" | "svg" => "XML", "json" | "jsonc" | "json5" => "JSON", "yaml" | "yml" => "YAML", "toml" => "TOML", "ini" | "cfg" | "conf" | "properties" | "env" => "INI", "md" | "markdown" | "mdx" => "Markdown", "rst" => "reStructuredText", "adoc" | "asciidoc" => "AsciiDoc", "txt" | "text" => "Text", "tex" | "cls" | "sty" => "LaTeX", "proto" => "Protocol Buffer", "thrift" => "Thrift", "graphql" | "gql" => "GraphQL", "tf" | "tfvars" | "hcl" => "Terraform", "nix" => "Nix", "cmake" => "CMake", "mk" | "am" => "Makefile", "gradle" => "Gradle", "groovy" | "gvy" | "gy" => "Groovy", "ex" | "exs" | "eex" => "Elixir", "erl" | "hrl" => "Erlang", "hs" | "lhs" => "Haskell", "ml" | "mli" => "OCaml", "clj" | "cljs" | "cljc" | "edn" => "Clojure", "nim" => "Nim", "zig" => "Zig", "jl" => "Julia", "dart" => "Dart", "v" => "V", "sol" => "Solidity", "vim" => "Vim Script", "el" => "Emacs Lisp", "bzl" | "bazel" => "Starlark", "bicep" => "Bicep", "mustache" | "hbs" => "Handlebars", "ejs" => "EJS", "twig" => "Twig", "liquid" => "Liquid", "patch" | "diff" => "Patch", _ => return special_file_label(&name), }); } special_file_label(&name) } fn special_file_label(name: &str) -> Option<&'static str> { Some(match name { "dockerfile" | "containerfile" => "Dockerfile", "makefile" | "gnumakefile" => "Makefile", "justfile" => "Just", "cmakelists.txt" => "CMake", "gemfile" | "rakefile" | "brewfile" | "vagrantfile" => "Ruby", "license" | "license-apache" | "license-mit" | "license-cc0" | "readme" | "changelog" => { "Text" } _ => return None, }) } /// 只有这几种语言接 tree-sitter 符号解析,其余语言仍可按内容检索。 fn tree_sitter_language(label: &str) -> Option { let language = match label { "Rust" => tree_sitter_rust::LANGUAGE.into(), "JavaScript" => tree_sitter_javascript::LANGUAGE.into(), "TypeScript" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), "TypeScript (React)" => tree_sitter_typescript::LANGUAGE_TSX.into(), "Python" => tree_sitter_python::LANGUAGE.into(), "Go" => tree_sitter_go::LANGUAGE.into(), "Java" => tree_sitter_java::LANGUAGE.into(), "C" => tree_sitter_c::LANGUAGE.into(), "C++" => tree_sitter_cpp::LANGUAGE.into(), _ => return None, }; Some(language) } pub fn symbols_for(path: &str, text: &str) -> Vec { let Some(label) = language_label(path) else { return Vec::new(); }; let Some(language) = tree_sitter_language(label) else { return Vec::new(); }; PARSER.with(|cell| { let mut guard = cell.borrow_mut(); let (parser, current) = guard.get_or_insert_with(|| (Parser::new(), "")); if *current != label { if parser.set_language(&language).is_err() { *current = ""; return Vec::new(); } *current = label; } 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) && let Ok(name) = node.utf8_text(source) && !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" ) }