eba4fd1
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,
}
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<Language> {
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<ParsedSymbol> {
let Some(language) = language_label(path).and_then(tree_sitter_language) 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<ParsedSymbol>) {
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<String> {
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<String> {
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"
)
}