use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, TryRecvError, channel};
use std::thread;
use ignore::WalkBuilder;
use tree_sitter_tags::{TagsConfiguration, TagsContext};
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: String,
pub line: usize,
}
pub struct LangConfigs {
rust: Option<TagsConfiguration>,
python: Option<TagsConfiguration>,
javascript: Option<TagsConfiguration>,
go: Option<TagsConfiguration>,
ruby: Option<TagsConfiguration>,
c: Option<TagsConfiguration>,
}
impl LangConfigs {
pub fn new() -> Self {
fn cfg(lang: tree_sitter::Language, query: &str) -> Option<TagsConfiguration> {
TagsConfiguration::new(lang, query, "").ok()
}
Self {
rust: cfg(tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::TAGS_QUERY),
python: cfg(tree_sitter_python::LANGUAGE.into(), tree_sitter_python::TAGS_QUERY),
javascript: cfg(
tree_sitter_javascript::LANGUAGE.into(),
tree_sitter_javascript::TAGS_QUERY,
),
go: cfg(tree_sitter_go::LANGUAGE.into(), tree_sitter_go::TAGS_QUERY),
ruby: cfg(tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::TAGS_QUERY),
c: cfg(tree_sitter_c::LANGUAGE.into(), tree_sitter_c::TAGS_QUERY),
}
}
fn for_ext(&self, ext: &str) -> Option<&TagsConfiguration> {
let cell = match ext {
"rs" => &self.rust,
"py" => &self.python,
"js" | "jsx" | "mjs" | "cjs" => &self.javascript,
"go" => &self.go,
"rb" => &self.ruby,
"c" | "h" => &self.c,
_ => return None,
};
cell.as_ref()
}
fn config_for(&self, path: &Path) -> Option<&TagsConfiguration> {
path.extension()
.and_then(|e| e.to_str())
.and_then(|ext| self.for_ext(ext))
}
pub fn file_symbols(&self, path: &Path, source: &[u8]) -> Vec<Symbol> {
let Some(config) = self.config_for(path) else {
return Vec::new();
};
let mut ctx = TagsContext::new();
let Ok((tags, _)) = ctx.generate_tags(config, source, None) else {
return Vec::new();
};
let mut out = Vec::new();
for tag in tags.flatten() {
if !tag.is_definition {
continue;
}
out.push(Symbol {
name: String::from_utf8_lossy(source.get(tag.name_range).unwrap_or_default())
.to_string(),
kind: config.syntax_type_name(tag.syntax_type_id).to_string(),
line: tag.span.start.row,
});
}
out.sort_by_key(|s| s.line);
out
}
}
type Def = (String, PathBuf, usize);
enum State {
Idle,
Building(Receiver<Vec<Def>>),
Ready(Vec<Def>),
}
pub struct ProjectIndex {
root: PathBuf,
state: State,
}
impl ProjectIndex {
pub fn new(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
state: State::Idle,
}
}
pub fn start(&mut self) {
if matches!(self.state, State::Idle) {
let (tx, rx) = channel();
let root = self.root.clone();
thread::spawn(move || {
let configs = LangConfigs::new();
let _ = tx.send(collect_defs(&root, &configs));
});
self.state = State::Building(rx);
}
}
pub fn poll(&mut self) -> bool {
if let State::Building(rx) = &self.state {
match rx.try_recv() {
Ok(defs) => {
self.state = State::Ready(defs);
return true;
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
self.state = State::Ready(Vec::new());
return true;
}
}
}
false
}
pub fn is_building(&self) -> bool {
matches!(self.state, State::Building(_))
}
pub fn definition(&self, name: &str) -> Option<(PathBuf, usize)> {
match &self.state {
State::Ready(defs) => defs
.iter()
.find(|(n, _, _)| n == name)
.map(|(_, p, l)| (p.clone(), *l)),
_ => None,
}
}
}
fn collect_defs(root: &Path, configs: &LangConfigs) -> Vec<Def> {
let mut defs = Vec::new();
for entry in WalkBuilder::new(root).build().flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(true) {
continue;
}
let path = entry.path();
let Ok(source) = std::fs::read(path) else {
continue;
};
for sym in configs.file_symbols(path, &source) {
defs.push((sym.name, path.to_path_buf(), sym.line));
}
}
defs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_symbols_lists_definitions_in_order() {
let configs = LangConfigs::new();
let path = PathBuf::from("lib.rs");
let src = b"struct A;\nfn foo() {}\nfn bar() {}\n";
let syms = configs.file_symbols(&path, src);
let names: Vec<_> = syms.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"A"));
assert!(names.contains(&"foo"));
assert!(names.contains(&"bar"));
assert!(syms.windows(2).all(|w| w[0].line <= w[1].line));
}
#[test]
fn project_index_resolves_definition() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut index = ProjectIndex::new(root.as_path());
index.start();
while index.is_building() {
std::thread::sleep(std::time::Duration::from_millis(20));
index.poll();
}
let (path, _line) = index
.definition("ProjectIndex")
.expect("ProjectIndex defined somewhere");
assert_eq!(path.extension().unwrap(), "rs");
}
}