use crate::error::AstError;
use crate::error::AstResult;
use crate::types::AstNode;
use crate::types::ParsedAst;
use dashmap::DashMap;
use std::path::Path;
use tree_sitter::Parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Language {
Rust,
Python,
JavaScript,
TypeScript,
Go,
Java,
C,
Cpp,
CSharp,
Bash,
Ruby,
Php,
Lua,
Haskell,
Elixir,
Scala,
OCaml,
Clojure,
Zig,
Swift,
Kotlin,
ObjectiveC,
R,
Julia,
Dart,
Wgsl,
Glsl,
}
impl Language {
pub fn parser(&self) -> tree_sitter::Language {
match self {
Self::Rust => tree_sitter_rust::LANGUAGE.into(),
Self::Python => tree_sitter_python::LANGUAGE.into(),
Self::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
Self::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
Self::Go => tree_sitter_go::LANGUAGE.into(),
Self::Java => tree_sitter_java::LANGUAGE.into(),
Self::C => tree_sitter_c::LANGUAGE.into(),
Self::Cpp => tree_sitter_cpp::LANGUAGE.into(),
Self::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
Self::Bash => tree_sitter_bash::LANGUAGE.into(),
Self::Ruby => tree_sitter_ruby::LANGUAGE.into(),
Self::Php => tree_sitter_php::LANGUAGE_PHP.into(),
Self::Lua => tree_sitter_lua::LANGUAGE.into(),
Self::Haskell => tree_sitter_haskell::LANGUAGE.into(),
Self::Elixir => tree_sitter_elixir::LANGUAGE.into(),
Self::Scala => tree_sitter_scala::LANGUAGE.into(),
Self::OCaml => tree_sitter_ocaml::LANGUAGE_OCAML.into(),
Self::Clojure => tree_sitter_clojure::LANGUAGE.into(),
Self::Zig => tree_sitter_zig::LANGUAGE.into(),
Self::Swift => tree_sitter_swift::LANGUAGE.into(),
Self::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
Self::ObjectiveC => tree_sitter_objc::LANGUAGE.into(),
Self::R => tree_sitter_bash::LANGUAGE.into(), Self::Julia => tree_sitter_bash::LANGUAGE.into(), Self::Dart => tree_sitter_bash::LANGUAGE.into(), Self::Wgsl => tree_sitter_bash::LANGUAGE.into(), Self::Glsl => tree_sitter_bash::LANGUAGE.into(), }
}
pub const fn is_fallback(&self) -> bool {
matches!(
self,
Self::R | Self::Julia | Self::Dart | Self::Wgsl | Self::Glsl
)
}
pub const fn actual_parser_name(&self) -> &'static str {
if !self.is_fallback() {
return self.name();
}
"Bash (fallback)"
}
pub const fn name(&self) -> &'static str {
match self {
Self::Rust => "Rust",
Self::Python => "Python",
Self::JavaScript => "JavaScript",
Self::TypeScript => "TypeScript",
Self::Go => "Go",
Self::Java => "Java",
Self::C => "C",
Self::Cpp => "C++",
Self::CSharp => "C#",
Self::Bash => "Bash",
Self::Ruby => "Ruby",
Self::Php => "PHP",
Self::Lua => "Lua",
Self::Haskell => "Haskell",
Self::Elixir => "Elixir",
Self::Scala => "Scala",
Self::OCaml => "OCaml",
Self::Clojure => "Clojure",
Self::Zig => "Zig",
Self::Swift => "Swift",
Self::Kotlin => "Kotlin",
Self::ObjectiveC => "Objective-C",
Self::R => "R",
Self::Julia => "Julia",
Self::Dart => "Dart",
Self::Wgsl => "WGSL",
Self::Glsl => "GLSL",
}
}
}
pub struct LanguageRegistry {
parsers: DashMap<Language, Parser>,
}
impl std::fmt::Debug for LanguageRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LanguageRegistry")
.field("parsers_count", &self.parsers.len())
.finish()
}
}
impl LanguageRegistry {
pub fn new() -> Self {
let registry = Self {
parsers: DashMap::new(),
};
let common_langs = [
Language::Rust,
Language::Python,
Language::JavaScript,
Language::TypeScript,
Language::Go,
Language::Java,
Language::C,
Language::Cpp,
];
for lang in common_langs {
let _ = registry.get_or_create_parser(lang);
}
registry
}
pub fn detect_language(&self, path: &Path) -> AstResult<Language> {
let extension = path
.extension()
.and_then(|e| e.to_str())
.ok_or_else(|| AstError::LanguageDetectionFailed(path.display().to_string()))?;
let lang = match extension {
"rs" => Language::Rust,
"py" | "pyi" => Language::Python,
"js" | "mjs" | "cjs" => Language::JavaScript,
"ts" | "mts" | "cts" => Language::TypeScript,
"tsx" | "jsx" => Language::TypeScript,
"go" => Language::Go,
"java" => Language::Java,
"c" | "h" => Language::C,
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "c++" => Language::Cpp,
"cs" => Language::CSharp,
"sh" | "bash" | "zsh" | "fish" => Language::Bash,
"rb" => Language::Ruby,
"php" => Language::Php,
"lua" => Language::Lua,
"hs" | "lhs" => Language::Haskell,
"ex" | "exs" => Language::Elixir,
"scala" | "sc" => Language::Scala,
"ml" | "mli" => Language::OCaml,
"clj" | "cljs" | "cljc" => Language::Clojure,
"zig" => Language::Zig,
"swift" => Language::Swift,
"kt" | "kts" => Language::Kotlin,
"m" | "mm" => Language::ObjectiveC,
"r" | "R" => Language::R,
"jl" => Language::Julia,
"dart" => Language::Dart,
"wgsl" => Language::Wgsl,
"glsl" | "vert" | "frag" => Language::Glsl,
"toml" | "yaml" | "yml" | "json" | "jsonc" | "xml" | "html" | "htm" | "css"
| "scss" | "sass" | "less" | "md" | "markdown" | "tex" | "latex" | "rst" | "sql"
| "graphql" | "gql" | "proto" | "dockerfile" | "makefile" | "cmake" | "hcl" | "tf"
| "tfvars" | "nix" => {
return Err(AstError::UnsupportedLanguage(format!(
"{} files should use patch-based editing, not AST operations",
extension
)));
}
_ => return Err(AstError::UnsupportedLanguage(extension.to_string())),
};
Ok(lang)
}
fn get_or_create_parser(&self, language: Language) -> Parser {
let mut parser = Parser::new();
let _ = parser.set_language(&language.parser());
parser
}
pub fn parse(&self, language: &Language, source: &str) -> AstResult<ParsedAst> {
let mut parser = self.get_or_create_parser(*language);
let tree = parser
.parse(source, None)
.ok_or_else(|| AstError::ParserError("Failed to parse source code".to_string()))?;
let root = tree.root_node();
let root_node = AstNode {
kind: root.kind().to_string(),
start_byte: root.start_byte(),
end_byte: root.end_byte(),
start_position: (root.start_position().row, root.start_position().column),
end_position: (root.end_position().row, root.end_position().column),
children_count: root.child_count(),
};
Ok(ParsedAst {
tree,
source: source.to_string(),
language: *language,
root_node,
})
}
pub fn stats(&self) -> LanguageRegistryStats {
LanguageRegistryStats {
loaded_parsers: self.parsers.len(),
total_languages: 27, }
}
}
impl Default for LanguageRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LanguageRegistryStats {
pub loaded_parsers: usize,
pub total_languages: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_language_detection() {
let registry = LanguageRegistry::new();
assert_eq!(
registry.detect_language(&PathBuf::from("test.rs")).unwrap(),
Language::Rust
);
assert_eq!(
registry.detect_language(&PathBuf::from("test.py")).unwrap(),
Language::Python
);
assert_eq!(
registry.detect_language(&PathBuf::from("test.js")).unwrap(),
Language::JavaScript
);
assert_eq!(
registry.detect_language(&PathBuf::from("test.ts")).unwrap(),
Language::TypeScript
);
assert!(
registry
.detect_language(&PathBuf::from("test.toml"))
.is_err()
);
assert!(
registry
.detect_language(&PathBuf::from("test.yaml"))
.is_err()
);
assert!(registry.detect_language(&PathBuf::from("test.md")).is_err());
assert!(
registry
.detect_language(&PathBuf::from("Dockerfile"))
.is_err()
);
}
#[test]
fn test_parsing() {
let registry = LanguageRegistry::new();
let rust_code = r#"
fn main() {
println!("Hello, world!");
}
"#;
let ast = registry.parse(&Language::Rust, rust_code).unwrap();
assert_eq!(ast.language, Language::Rust);
assert_eq!(ast.root_node.kind, "source_file");
assert!(ast.root_node.children_count > 0);
}
#[test]
fn test_fallback_detection() {
assert!(!Language::Rust.is_fallback());
assert!(!Language::Python.is_fallback());
assert!(Language::R.is_fallback());
assert!(Language::Julia.is_fallback());
assert!(Language::Wgsl.is_fallback());
}
}