use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use super::{LanguageBehavior, LanguageParser};
use crate::{IndexResult, Settings};
pub type ParserBehaviorPair = (Box<dyn LanguageParser>, Box<dyn LanguageBehavior>);
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LanguageId(&'static str);
impl LanguageId {
pub const fn new(id: &'static str) -> Self {
Self(id)
}
pub fn as_str(&self) -> &'static str {
self.0
}
}
impl std::fmt::Display for LanguageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for LanguageId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.0)
}
}
impl<'de> Deserialize<'de> for LanguageId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let static_str = match s.as_str() {
"c" => "c",
"clojure" => "clojure",
"cpp" => "cpp",
"csharp" => "csharp",
"gdscript" => "gdscript",
"go" => "go",
"java" => "java",
"javascript" => "javascript",
"kotlin" => "kotlin",
"lua" => "lua",
"php" => "php",
"python" => "python",
"rust" => "rust",
"swift" => "swift",
"typescript" => "typescript",
_ => Box::leak(s.into_boxed_str()),
};
Ok(LanguageId(static_str))
}
}
#[derive(Error, Debug)]
pub enum RegistryError {
#[error(
"Language '{0}' not found in registry\nSuggestion: Check available languages with 'codanna list-languages' or ensure the language module is compiled in"
)]
LanguageNotFound(LanguageId),
#[error(
"Language '{0}' is available but disabled\nSuggestion: Enable it in .codanna/settings.toml by setting languages.{0}.enabled = true"
)]
LanguageDisabled(LanguageId),
#[error(
"No language found for extension '.{0}'\nSuggestion: Check if the file type is supported or add a language mapping in settings.toml"
)]
ExtensionNotMapped(String),
#[error(
"Failed to create parser for language '{language}': {reason}\nSuggestion: Check the language configuration in settings.toml"
)]
ParserCreationFailed {
language: LanguageId,
reason: String,
},
}
pub trait LanguageDefinition: Send + Sync {
fn id(&self) -> LanguageId;
fn name(&self) -> &'static str;
fn extensions(&self) -> &'static [&'static str];
fn create_parser(&self, settings: &Settings) -> IndexResult<Box<dyn LanguageParser>>;
fn create_behavior(&self) -> Box<dyn LanguageBehavior>;
fn default_enabled(&self) -> bool {
false }
fn is_enabled(&self, settings: &Settings) -> bool {
settings
.languages
.get(self.id().as_str())
.map(|config| config.enabled)
.unwrap_or(false)
}
}
pub struct LanguageRegistry {
definitions: HashMap<LanguageId, Arc<dyn LanguageDefinition>>,
extension_map: HashMap<&'static str, LanguageId>,
}
impl LanguageRegistry {
pub fn new() -> Self {
Self {
definitions: HashMap::new(),
extension_map: HashMap::new(),
}
}
pub fn register(&mut self, definition: Arc<dyn LanguageDefinition>) {
let id = definition.id();
self.definitions.insert(id, definition.clone());
for ext in definition.extensions() {
self.extension_map.insert(ext, id);
}
}
#[must_use]
pub fn get(&self, id: LanguageId) -> Option<&dyn LanguageDefinition> {
self.definitions.get(&id).map(|def| def.as_ref())
}
#[must_use]
pub fn get_by_extension(&self, extension: &str) -> Option<&dyn LanguageDefinition> {
let ext = extension.strip_prefix('.').unwrap_or(extension);
self.extension_map.get(ext).and_then(|id| self.get(*id))
}
#[must_use]
pub fn find_language_id(&self, name: &str) -> Option<LanguageId> {
for def in self.definitions.values() {
if def.id().as_str() == name {
return Some(def.id());
}
}
None
}
pub fn iter_all(&self) -> impl Iterator<Item = &dyn LanguageDefinition> {
self.definitions.values().map(|def| def.as_ref())
}
pub fn iter_enabled<'a>(
&'a self,
settings: &'a Settings,
) -> impl Iterator<Item = &'a dyn LanguageDefinition> {
self.iter_all().filter(move |def| def.is_enabled(settings))
}
pub fn enabled_extensions<'a>(
&'a self,
settings: &'a Settings,
) -> impl Iterator<Item = &'static str> + 'a {
self.iter_enabled(settings)
.flat_map(|def| def.extensions().iter().copied())
}
#[must_use]
pub fn is_available(&self, id: LanguageId) -> bool {
self.definitions.contains_key(&id)
}
#[must_use]
pub fn is_enabled(&self, id: LanguageId, settings: &Settings) -> bool {
self.get(id)
.map(|def| def.is_enabled(settings))
.unwrap_or(false)
}
pub fn create_parser(
&self,
id: LanguageId,
settings: &Settings,
) -> Result<Box<dyn LanguageParser>, RegistryError> {
match self.get(id) {
None => Err(RegistryError::LanguageNotFound(id)),
Some(def) => {
if !def.is_enabled(settings) {
return Err(RegistryError::LanguageDisabled(id));
}
def.create_parser(settings)
.map_err(|e| RegistryError::ParserCreationFailed {
language: id,
reason: e.to_string(),
})
}
}
}
pub fn create_parser_with_behavior(
&self,
id: LanguageId,
settings: &Settings,
) -> Result<ParserBehaviorPair, RegistryError> {
match self.get(id) {
None => Err(RegistryError::LanguageNotFound(id)),
Some(def) => {
if !def.is_enabled(settings) {
return Err(RegistryError::LanguageDisabled(id));
}
let parser = def.create_parser(settings).map_err(|e| {
RegistryError::ParserCreationFailed {
language: id,
reason: e.to_string(),
}
})?;
let behavior = def.create_behavior();
Ok((parser, behavior))
}
}
}
}
impl Default for LanguageRegistry {
fn default() -> Self {
Self::new()
}
}
use std::sync::LazyLock;
static REGISTRY: LazyLock<std::sync::Mutex<LanguageRegistry>> = LazyLock::new(|| {
let mut registry = LanguageRegistry::new();
initialize_registry(&mut registry);
std::sync::Mutex::new(registry)
});
fn initialize_registry(registry: &mut LanguageRegistry) {
super::rust::register(registry);
super::python::register(registry);
super::php::register(registry);
super::typescript::register(registry);
super::javascript::register(registry);
super::go::register(registry);
super::c::register(registry);
super::cpp::register(registry);
super::csharp::register(registry);
super::gdscript::register(registry);
super::java::register(registry);
super::kotlin::register(registry);
super::clojure::register(registry);
super::lua::register(registry);
super::swift::register(registry);
}
pub fn get_registry() -> &'static std::sync::Mutex<LanguageRegistry> {
®ISTRY
}
#[cfg(test)]
mod tests {
use super::*;
struct MockLanguage {
id: LanguageId,
enabled: bool,
}
impl LanguageDefinition for MockLanguage {
fn id(&self) -> LanguageId {
self.id
}
fn name(&self) -> &'static str {
"Mock Language"
}
fn extensions(&self) -> &'static [&'static str] {
&["mock", "test"]
}
fn create_parser(&self, _settings: &Settings) -> IndexResult<Box<dyn LanguageParser>> {
unimplemented!("Mock parser creation")
}
fn create_behavior(&self) -> Box<dyn LanguageBehavior> {
unimplemented!("Mock behavior creation")
}
fn is_enabled(&self, _settings: &Settings) -> bool {
self.enabled
}
}
#[test]
fn test_language_id() {
let id1 = LanguageId::new("rust");
let id2 = LanguageId::new("rust");
let id3 = LanguageId::new("python");
assert_eq!(id1, id2);
assert_ne!(id1, id3);
assert_eq!(id1.as_str(), "rust");
assert_eq!(format!("{id1}"), "rust");
}
#[test]
fn test_registry_registration() {
let mut registry = LanguageRegistry::new();
let mock = Arc::new(MockLanguage {
id: LanguageId::new("mock"),
enabled: true,
});
registry.register(mock);
assert!(registry.is_available(LanguageId::new("mock")));
assert!(!registry.is_available(LanguageId::new("unknown")));
assert!(registry.get_by_extension("mock").is_some());
assert!(registry.get_by_extension("test").is_some());
assert!(registry.get_by_extension(".mock").is_some()); assert!(registry.get_by_extension("unknown").is_none());
}
#[test]
fn test_registry_iteration() {
let mut registry = LanguageRegistry::new();
registry.register(Arc::new(MockLanguage {
id: LanguageId::new("enabled"),
enabled: true,
}));
registry.register(Arc::new(MockLanguage {
id: LanguageId::new("disabled"),
enabled: false,
}));
assert_eq!(registry.iter_all().count(), 2);
let settings = Settings::default();
let enabled: Vec<_> = registry.iter_enabled(&settings).collect();
assert_eq!(enabled.len(), 1);
assert_eq!(enabled[0].id(), LanguageId::new("enabled"));
}
#[test]
fn test_global_registry_initialization() {
let registry = get_registry();
let registry = registry.lock().unwrap();
assert!(registry.is_available(LanguageId::new("rust")));
assert!(registry.is_available(LanguageId::new("python")));
assert!(registry.is_available(LanguageId::new("php")));
assert!(registry.is_available(LanguageId::new("go")));
assert!(registry.get_by_extension("rs").is_some());
assert!(registry.get_by_extension("py").is_some());
assert!(registry.get_by_extension("php").is_some());
assert!(registry.get_by_extension("go").is_some());
let rust = registry.get(LanguageId::new("rust")).unwrap();
assert_eq!(rust.name(), "Rust");
let python = registry.get(LanguageId::new("python")).unwrap();
assert_eq!(python.name(), "Python");
let php = registry.get(LanguageId::new("php")).unwrap();
assert_eq!(php.name(), "PHP");
let go = registry.get(LanguageId::new("go")).unwrap();
assert_eq!(go.name(), "Go");
}
}