use tree_sitter::Language;
pub struct ManifestSpec {
pub filename: &'static str,
pub name_key: &'static str,
pub self_names: &'static [&'static str],
pub normalize: fn(&str) -> String,
}
#[derive(Debug, Clone)]
pub struct ModuleRoot {
pub name: String,
pub dir: String,
pub language: &'static str,
}
pub fn manifest_root(rel_path: &str, content: &str) -> Option<ModuleRoot> {
let base = rel_path.rsplit('/').next()?;
let spec = LANGUAGES
.iter()
.find(|l| l.manifest.is_some_and(|m| m.filename == base))?;
let m = spec.manifest?;
let name = content.lines().find_map(|line| {
let rest = line.trim().strip_prefix(m.name_key)?;
let rest = rest.trim_start();
let rest = rest.strip_prefix('=').unwrap_or(rest).trim();
let name = rest.trim_matches('"').trim();
(!name.is_empty() && !name.contains(' ')).then(|| name.to_string())
})?;
let dir = rel_path
.rsplit_once('/')
.map(|(d, _)| d.to_string())
.unwrap_or_default();
Some(ModuleRoot {
name: (m.normalize)(&name),
dir,
language: spec.name,
})
}
pub struct InlineSpec {
pub grammar: fn() -> Language,
pub query_source: &'static str,
pub container_kinds: &'static [&'static str],
}
pub struct LanguageSpec {
pub name: &'static str,
pub extensions: &'static [&'static str],
pub grammar: fn() -> Language,
pub query_source: &'static str,
pub comment_kinds: &'static [&'static str],
pub module_path: fn(&str) -> Vec<String>,
pub path_separators: &'static [&'static str],
pub absolutize: fn(path: &str, file: &str) -> Vec<String>,
pub receivers: &'static [&'static str],
pub doc_skip_kinds: &'static [&'static str],
pub manifest: Option<&'static ManifestSpec>,
pub inline: Option<&'static InlineSpec>,
pub file_refs: bool,
pub implicit_interfaces: bool,
}
fn rust_normalize(name: &str) -> String {
name.replace('-', "_")
}
static RUST_MANIFEST: ManifestSpec = ManifestSpec {
filename: "Cargo.toml",
name_key: "name",
self_names: &["crate"],
normalize: rust_normalize,
};
fn identity_normalize(name: &str) -> String {
name.to_string()
}
static GO_MANIFEST: ManifestSpec = ManifestSpec {
filename: "go.mod",
name_key: "module",
self_names: &[],
normalize: identity_normalize,
};
fn split_all(path: &str, separators: &[&str]) -> Vec<String> {
let mut segments = vec![path.to_string()];
for sep in separators {
segments = segments
.iter()
.flat_map(|s| s.split(sep).map(str::to_string))
.collect();
}
segments.into_iter().filter(|s| !s.is_empty()).collect()
}
fn dirname_segments(file: &str) -> Vec<String> {
let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
segments.pop();
segments
}
fn rust_absolutize(path: &str, file: &str) -> Vec<String> {
let mut module = rust_module_path(file);
let mut rest = path;
if let Some(r) = rest.strip_prefix("self::") {
rest = r;
} else {
while let Some(r) = rest.strip_prefix("super::") {
module.pop();
rest = r;
}
if rest.len() == path.len() {
if path.starts_with("crate::") {
return split_all(path, &["::", "."]);
}
module.extend(split_all(path, &["::", "."]));
return module;
}
}
module.extend(split_all(rest, &["::", "."]));
module
}
fn go_absolutize(path: &str, _file: &str) -> Vec<String> {
split_all(path, &["/", "."])
}
fn proto_absolutize(path: &str, _file: &str) -> Vec<String> {
split_all(path.strip_suffix(".proto").unwrap_or(path), &["/", "."])
}
fn python_absolutize(path: &str, file: &str) -> Vec<String> {
let dots = path.len() - path.trim_start_matches('.').len();
if dots == 0 {
return split_all(path, &["."]);
}
let mut base = dirname_segments(file);
for _ in 1..dots {
base.pop();
}
base.extend(split_all(&path[dots..], &["."]));
base
}
fn typescript_absolutize(path: &str, file: &str) -> Vec<String> {
if !path.starts_with('.') {
return split_all(path, &["/", "."]);
}
let mut base = dirname_segments(file);
let mut rest = path;
if let Some(r) = rest.strip_prefix('/') {
base.clear();
rest = r;
}
loop {
if let Some(r) = rest.strip_prefix("./") {
rest = r;
} else if let Some(r) = rest.strip_prefix("../") {
base.pop();
rest = r;
} else {
break;
}
}
base.extend(split_all(rest, &["/"]));
base
}
fn rust_grammar() -> Language {
tree_sitter_rust::LANGUAGE.into()
}
fn go_grammar() -> Language {
tree_sitter_go::LANGUAGE.into()
}
fn rust_module_path(file: &str) -> Vec<String> {
let trimmed = file.strip_suffix(".rs").unwrap_or(file);
let after_src = trimmed.rsplit_once("src/").map_or(trimmed, |(_, r)| r);
let mut segments = vec!["crate".to_string()];
for seg in after_src.split('/') {
if !matches!(seg, "lib" | "main" | "mod" | "") {
segments.push(seg.to_string());
}
}
segments
}
fn go_module_path(file: &str) -> Vec<String> {
let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
segments.pop(); segments
}
fn python_grammar() -> Language {
tree_sitter_python::LANGUAGE.into()
}
fn bash_grammar() -> Language {
tree_sitter_bash::LANGUAGE.into()
}
fn bash_module_path(file: &str) -> Vec<String> {
let trimmed = file
.strip_suffix(".sh")
.or_else(|| file.strip_suffix(".bash"))
.unwrap_or(file);
trimmed
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn bash_absolutize(path: &str, file: &str) -> Vec<String> {
let trimmed = path.trim();
let dir_relative = [
"$(dirname \"$0\")/",
"$(dirname $0)/",
"${BASH_SOURCE%/*}/",
"./",
]
.iter()
.find_map(|p| trimmed.strip_prefix(p));
let stripped = |s: &str| {
s.strip_suffix(".sh")
.or_else(|| s.strip_suffix(".bash"))
.unwrap_or(s)
.to_string()
};
match dir_relative {
Some(rest) => {
let mut base = dirname_segments(file);
base.extend(rest.split('/').filter(|s| !s.is_empty()).map(stripped));
base
}
None => trimmed
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.map(stripped)
.collect(),
}
}
fn cpp_grammar() -> Language {
tree_sitter_cpp::LANGUAGE.into()
}
fn cpp_module_path(file: &str) -> Vec<String> {
let trimmed = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
trimmed
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn cpp_absolutize(path: &str, file: &str) -> Vec<String> {
let trimmed = path.trim().trim_matches(['<', '>']);
let no_ext = trimmed.rsplit_once('.').map_or(trimmed, |(stem, ext)| {
if matches!(
ext,
"h" | "hh" | "hpp" | "hxx" | "cpp" | "cc" | "cxx" | "inl"
) {
stem
} else {
trimmed
}
});
if let Some(rest) = no_ext.strip_prefix("./") {
let mut base = dirname_segments(file);
base.extend(
rest.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string),
);
return base;
}
no_ext
.replace("->", ".")
.split(['/', ':', '.'])
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn proto_grammar() -> Language {
tree_sitter_proto::LANGUAGE.into()
}
fn proto_module_path(file: &str) -> Vec<String> {
let trimmed = file.strip_suffix(".proto").unwrap_or(file);
trimmed
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn typescript_grammar() -> Language {
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
}
fn python_module_path(file: &str) -> Vec<String> {
let trimmed = file.strip_suffix(".py").unwrap_or(file);
trimmed
.split('/')
.filter(|s| !matches!(*s, "__init__" | ""))
.map(str::to_string)
.collect()
}
fn typescript_module_path(file: &str) -> Vec<String> {
let trimmed = file
.strip_suffix(".tsx")
.or_else(|| file.strip_suffix(".ts"))
.unwrap_or(file);
trimmed
.split('/')
.filter(|s| !matches!(*s, "index" | ""))
.map(str::to_string)
.collect()
}
fn javascript_grammar() -> Language {
tree_sitter_javascript::LANGUAGE.into()
}
fn c_grammar() -> Language {
tree_sitter_c::LANGUAGE.into()
}
fn java_grammar() -> Language {
tree_sitter_java::LANGUAGE.into()
}
fn csharp_grammar() -> Language {
tree_sitter_c_sharp::LANGUAGE.into()
}
fn csharp_module_path(file: &str) -> Vec<String> {
let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
segments.pop(); segments
}
fn markdown_grammar() -> Language {
tree_sitter_md::LANGUAGE.into()
}
fn markdown_inline_grammar() -> Language {
tree_sitter_md::INLINE_LANGUAGE.into()
}
static MARKDOWN_INLINE: InlineSpec = InlineSpec {
grammar: markdown_inline_grammar,
query_source: include_str!("../queries/markdown-inline.scm"),
container_kinds: &["inline"],
};
fn markdown_absolutize(path: &str, file: &str) -> Vec<String> {
let mut base = dirname_segments(file);
let mut rest = path;
if let Some(r) = rest.strip_prefix('/') {
base.clear();
rest = r;
}
loop {
if let Some(r) = rest.strip_prefix("./") {
rest = r;
} else if let Some(r) = rest.strip_prefix("../") {
base.pop();
rest = r;
} else {
break;
}
}
let rest = rest
.strip_suffix(".md")
.or_else(|| rest.strip_suffix(".markdown"))
.unwrap_or(rest);
base.extend(
rest.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string),
);
base
}
fn markdown_module_path(file: &str) -> Vec<String> {
let trimmed = file
.strip_suffix(".md")
.or_else(|| file.strip_suffix(".markdown"))
.unwrap_or(file);
trimmed
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn sql_grammar() -> Language {
tree_sitter_sequel::LANGUAGE.into()
}
fn javascript_module_path(file: &str) -> Vec<String> {
let trimmed = file
.strip_suffix(".jsx")
.or_else(|| file.strip_suffix(".mjs"))
.or_else(|| file.strip_suffix(".cjs"))
.or_else(|| file.strip_suffix(".js"))
.unwrap_or(file);
trimmed
.split('/')
.filter(|s| !matches!(*s, "index" | ""))
.map(str::to_string)
.collect()
}
fn javascript_absolutize(path: &str, file: &str) -> Vec<String> {
typescript_absolutize(path, file)
}
fn c_module_path(file: &str) -> Vec<String> {
cpp_module_path(file)
}
fn c_absolutize(path: &str, file: &str) -> Vec<String> {
cpp_absolutize(path, file)
}
fn java_module_path(file: &str) -> Vec<String> {
dirname_segments(file)
.into_iter()
.filter(|s| !s.is_empty())
.collect()
}
fn java_absolutize(path: &str, _file: &str) -> Vec<String> {
let head = path.split('(').next().unwrap_or(path);
head.split('.')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn sql_module_path(file: &str) -> Vec<String> {
let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
dir.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn dotted_absolutize(path: &str, _file: &str) -> Vec<String> {
path.trim()
.split('.')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
pub static LANGUAGES: &[LanguageSpec] = &[
LanguageSpec {
name: "rust",
extensions: &["rs"],
grammar: rust_grammar,
query_source: include_str!("../queries/rust.scm"),
comment_kinds: &["line_comment", "block_comment"],
module_path: rust_module_path,
path_separators: &["::", "."],
absolutize: rust_absolutize,
receivers: &["self", "Self"],
doc_skip_kinds: &[],
manifest: Some(&RUST_MANIFEST),
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "go",
extensions: &["go"],
grammar: go_grammar,
query_source: include_str!("../queries/go.scm"),
comment_kinds: &["comment"],
module_path: go_module_path,
path_separators: &["/", "."],
absolutize: go_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: Some(&GO_MANIFEST),
inline: None,
file_refs: false,
implicit_interfaces: true,
},
LanguageSpec {
name: "python",
extensions: &["py"],
grammar: python_grammar,
query_source: include_str!("../queries/python.scm"),
comment_kinds: &["comment"],
module_path: python_module_path,
path_separators: &["."],
absolutize: python_absolutize,
receivers: &["self", "cls"],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "typescript",
extensions: &["ts", "tsx"],
grammar: typescript_grammar,
query_source: include_str!("../queries/typescript.scm"),
comment_kinds: &["comment"],
module_path: typescript_module_path,
path_separators: &["/", "."],
absolutize: typescript_absolutize,
receivers: &["this"],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "bash",
extensions: &["sh", "bash"],
grammar: bash_grammar,
query_source: include_str!("../queries/bash.scm"),
comment_kinds: &["comment"],
module_path: bash_module_path,
path_separators: &["/"],
absolutize: bash_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "proto",
extensions: &["proto"],
grammar: proto_grammar,
query_source: include_str!("../queries/proto.scm"),
comment_kinds: &["comment"],
module_path: proto_module_path,
path_separators: &["/", "."],
absolutize: proto_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "cpp",
extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
grammar: cpp_grammar,
query_source: include_str!("../queries/cpp.scm"),
comment_kinds: &["comment"],
module_path: cpp_module_path,
path_separators: &["/", "::"],
absolutize: cpp_absolutize,
receivers: &["this"],
doc_skip_kinds: &["expression_statement"],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "javascript",
extensions: &["js", "jsx", "mjs", "cjs"],
grammar: javascript_grammar,
query_source: include_str!("../queries/javascript.scm"),
comment_kinds: &["comment"],
module_path: javascript_module_path,
path_separators: &["/", "."],
absolutize: javascript_absolutize,
receivers: &["this"],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "c",
extensions: &["c"],
grammar: c_grammar,
query_source: include_str!("../queries/c.scm"),
comment_kinds: &["comment"],
module_path: c_module_path,
path_separators: &["/"],
absolutize: c_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "java",
extensions: &["java"],
grammar: java_grammar,
query_source: include_str!("../queries/java.scm"),
comment_kinds: &["line_comment", "block_comment"],
module_path: java_module_path,
path_separators: &["."],
absolutize: java_absolutize,
receivers: &["this"],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "csharp",
extensions: &["cs"],
grammar: csharp_grammar,
query_source: include_str!("../queries/csharp.scm"),
comment_kinds: &["comment"],
module_path: csharp_module_path,
path_separators: &["."],
absolutize: dotted_absolutize,
receivers: &["this", "base"],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "sql",
extensions: &["sql"],
grammar: sql_grammar,
query_source: include_str!("../queries/sql.scm"),
comment_kinds: &["comment", "marginalia"],
module_path: sql_module_path,
path_separators: &["."],
absolutize: dotted_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: None,
inline: None,
file_refs: false,
implicit_interfaces: false,
},
LanguageSpec {
name: "markdown",
extensions: &["md", "markdown"],
grammar: markdown_grammar,
query_source: include_str!("../queries/markdown.scm"),
comment_kinds: &[],
module_path: markdown_module_path,
path_separators: &["/"],
absolutize: markdown_absolutize,
receivers: &[],
doc_skip_kinds: &[],
manifest: None,
inline: Some(&MARKDOWN_INLINE),
file_refs: true,
implicit_interfaces: false,
},
];
pub fn spec_for_path(path: &str) -> Option<&'static LanguageSpec> {
let ext = path.rsplit('.').next()?;
LANGUAGES.iter().find(|spec| spec.extensions.contains(&ext))
}