use tree_sitter_tags::{TagsConfiguration, TagsContext};
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: String,
pub line: usize,
pub is_definition: bool,
}
fn lang_and_query(ext: &str) -> Option<(tree_sitter::Language, &'static str)> {
let pair = match ext {
"rs" => (tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::TAGS_QUERY),
"js" | "mjs" | "cjs" | "jsx" => {
(tree_sitter_javascript::LANGUAGE.into(), tree_sitter_javascript::TAGS_QUERY)
}
"ts" => {
(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), tree_sitter_typescript::TAGS_QUERY)
}
"tsx" => (tree_sitter_typescript::LANGUAGE_TSX.into(), tree_sitter_typescript::TAGS_QUERY),
"py" | "pyi" => (tree_sitter_python::LANGUAGE.into(), tree_sitter_python::TAGS_QUERY),
"ex" | "exs" => (tree_sitter_elixir::LANGUAGE.into(), tree_sitter_elixir::TAGS_QUERY),
"go" => (tree_sitter_go::LANGUAGE.into(), tree_sitter_go::TAGS_QUERY),
"c" | "h" => (tree_sitter_c::LANGUAGE.into(), tree_sitter_c::TAGS_QUERY),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
(tree_sitter_cpp::LANGUAGE.into(), tree_sitter_cpp::TAGS_QUERY)
}
"java" => (tree_sitter_java::LANGUAGE.into(), tree_sitter_java::TAGS_QUERY),
"rb" => (tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::TAGS_QUERY),
"cs" => (tree_sitter_c_sharp::LANGUAGE.into(), tree_sitter_c_sharp::TAGS_QUERY),
"php" => (tree_sitter_php::LANGUAGE_PHP.into(), tree_sitter_php::TAGS_QUERY),
"lua" => (tree_sitter_lua::LANGUAGE.into(), tree_sitter_lua::TAGS_QUERY),
_ => return None,
};
Some(pair)
}
pub fn supports(ext: &str) -> bool {
lang_and_query(ext).is_some()
}
pub fn config_for(ext: &str) -> Option<TagsConfiguration> {
let (language, query) = lang_and_query(ext)?;
TagsConfiguration::new(language, query, "").ok()
}
pub fn extract(context: &mut TagsContext, config: &TagsConfiguration, text: &str) -> Vec<Symbol> {
collect(context, config, text, true)
}
pub fn extract_all(
context: &mut TagsContext,
config: &TagsConfiguration,
text: &str,
) -> Vec<Symbol> {
collect(context, config, text, false)
}
fn collect(
context: &mut TagsContext,
config: &TagsConfiguration,
text: &str,
definitions_only: bool,
) -> Vec<Symbol> {
let source = text.as_bytes();
let mut out = Vec::new();
let Ok((tags, _)) = context.generate_tags(config, source, None) else {
return out;
};
for tag in tags.flatten() {
if definitions_only && !tag.is_definition {
continue;
}
let name = String::from_utf8_lossy(&source[tag.name_range.clone()]).into_owned();
if name.is_empty() {
continue;
}
out.push(Symbol {
name,
kind: config.syntax_type_name(tag.syntax_type_id).to_string(),
line: tag.span.start.row + 1,
is_definition: tag.is_definition,
});
}
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
#[test]
fn extracts_rust_symbols() {
let mut ctx = TagsContext::new();
let cfg = config_for("rs").expect("rust config");
let src = "pub fn alpha() {}\nstruct Beta;\nimpl Beta { fn gamma(&self) {} }\n";
let syms = extract(&mut ctx, &cfg, src);
let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"alpha"), "got {names:?}");
assert!(names.contains(&"Beta"), "got {names:?}");
assert!(names.contains(&"gamma"), "got {names:?}");
assert_eq!(syms.iter().find(|s| s.name == "alpha").map(|s| s.line), Some(1));
}
#[test]
fn unsupported_extension_has_no_config() {
assert!(config_for("json").is_none());
assert!(!supports("css"));
assert!(supports("rs"));
assert!(supports("ts"));
assert!(supports("py"));
assert!(supports("ex"));
}
#[test]
fn extracts_python_symbols() {
let mut ctx = TagsContext::new();
let cfg = config_for("py").expect("python config");
let src = "class Greeter:\n def hello(self, name):\n return name\n\ndef top():\n return Greeter()\n";
let syms = extract(&mut ctx, &cfg, src);
let names: Vec<&str> = syms.iter().map(|symbol| symbol.name.as_str()).collect();
assert!(names.contains(&"Greeter"), "got {names:?}");
assert!(names.contains(&"hello"), "got {names:?}");
assert!(names.contains(&"top"), "got {names:?}");
}
#[test]
fn extracts_elixir_symbols() {
let mut ctx = TagsContext::new();
let cfg = config_for("ex").expect("elixir config");
let src = "defmodule Demo do\n def hello(name), do: name\n defmacro traced(expr), do: expr\nend\n";
let syms = extract(&mut ctx, &cfg, src);
let names: Vec<&str> = syms.iter().map(|symbol| symbol.name.as_str()).collect();
assert!(names.contains(&"Demo"), "got {names:?}");
assert!(names.contains(&"hello"), "got {names:?}");
assert!(names.contains(&"traced"), "got {names:?}");
}
}