use super::types::LanguagePlugin;
use std::collections::HashMap;
use std::path::Path;
pub struct PluginManager {
builtin_plugins: Vec<Box<dyn LanguagePlugin>>,
extension_cache: HashMap<String, usize>,
id_cache: HashMap<String, usize>,
plugin_state_hash: u64,
}
impl PluginManager {
#[must_use]
pub fn new() -> Self {
Self::with_plugins(Vec::new())
}
#[cfg(test)]
#[must_use]
pub fn empty() -> Self {
Self::with_plugins(Vec::new())
}
#[must_use]
pub fn with_plugins(plugins: Vec<Box<dyn LanguagePlugin>>) -> Self {
let plugin_state_hash = Self::compute_plugin_hash_static(&plugins);
let mut manager = Self {
builtin_plugins: plugins,
extension_cache: HashMap::new(),
id_cache: HashMap::new(),
plugin_state_hash,
};
manager.rebuild_caches();
manager
}
pub fn register_builtin(&mut self, plugin: Box<dyn LanguagePlugin>) {
let index = self.builtin_plugins.len();
self.builtin_plugins.push(plugin);
let plugin_ref = &self.builtin_plugins[index];
let metadata = plugin_ref.metadata();
self.id_cache.insert(metadata.id.to_string(), index);
for ext in plugin_ref.extensions() {
self.extension_cache.insert((*ext).to_string(), index);
}
self.update_plugin_hash();
}
#[must_use]
pub fn plugin_for_extension(&self, ext: &str) -> Option<&dyn LanguagePlugin> {
let ext = ext.to_ascii_lowercase();
self.extension_cache
.get(ext.as_str())
.and_then(|&index| self.builtin_plugins.get(index).map(|p| &**p))
}
#[must_use]
pub fn plugin_for_path(&self, path: &Path) -> Option<&dyn LanguagePlugin> {
let filename = path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.trim_start_matches('.').to_ascii_lowercase());
if let Some(name) = filename.as_deref()
&& name.starts_with("pulumi.")
{
let ext = Path::new(name).extension().and_then(|e| e.to_str());
if matches!(ext, Some("yaml" | "yml" | "json"))
&& let Some(plugin) = self.plugin_by_id("pulumi")
{
return Some(plugin);
}
}
if let Some(ext) = path.extension().and_then(|e| e.to_str())
&& let Some(plugin) = self.plugin_for_extension(ext)
{
return Some(plugin);
}
filename
.as_deref()
.and_then(|name| self.plugin_for_extension(name))
}
#[must_use]
pub fn plugin_by_id(&self, id: &str) -> Option<&dyn LanguagePlugin> {
self.id_cache
.get(id)
.and_then(|&index| self.builtin_plugins.get(index).map(|p| &**p))
}
#[must_use]
pub fn plugins(&self) -> Vec<&dyn LanguagePlugin> {
self.builtin_plugins.iter().map(|p| &**p).collect()
}
fn compute_plugin_hash_static(plugins: &[Box<dyn LanguagePlugin>]) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
let mut plugin_data: Vec<_> = plugins
.iter()
.map(|p| {
let meta = p.metadata();
(meta.id.to_string(), meta.version.to_string())
})
.collect();
plugin_data.sort_by(|a, b| a.0.cmp(&b.0));
for (id, version) in plugin_data {
hasher.write(id.as_bytes());
hasher.write(version.as_bytes());
}
hasher.finish()
}
#[must_use]
pub fn plugin_state_hash(&self) -> u64 {
self.plugin_state_hash
}
fn update_plugin_hash(&mut self) {
self.plugin_state_hash = Self::compute_plugin_hash_static(&self.builtin_plugins);
}
fn rebuild_caches(&mut self) {
self.extension_cache.clear();
self.id_cache.clear();
for (index, plugin) in self.builtin_plugins.iter().enumerate() {
let metadata = plugin.metadata();
self.id_cache.insert(metadata.id.to_string(), index);
for ext in plugin.extensions() {
self.extension_cache.insert((*ext).to_string(), index);
}
}
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Scope;
use crate::plugin::LanguageMetadata;
use std::path::Path;
struct MockPlugin {
id: &'static str,
name: &'static str,
extensions: &'static [&'static str],
}
impl MockPlugin {
fn new(id: &'static str, name: &'static str, extensions: &'static [&'static str]) -> Self {
Self {
id,
name,
extensions,
}
}
}
impl LanguagePlugin for MockPlugin {
fn metadata(&self) -> LanguageMetadata {
LanguageMetadata {
id: self.id,
name: self.name,
version: "1.0.0",
author: "Test",
description: "Mock plugin for testing",
tree_sitter_version: "0.24",
}
}
fn extensions(&self) -> &'static [&'static str] {
self.extensions
}
fn language(&self) -> tree_sitter::Language {
tree_sitter_rust::LANGUAGE.into()
}
fn parse_ast(
&self,
_content: &[u8],
) -> Result<tree_sitter::Tree, super::super::error::ParseError> {
Err(super::super::error::ParseError::TreeSitterFailed)
}
fn extract_scopes(
&self,
_tree: &tree_sitter::Tree,
_content: &[u8],
_file_path: &Path,
) -> Result<Vec<Scope>, super::super::error::ScopeError> {
Ok(Vec::new())
}
}
#[test]
fn test_empty_manager() {
let manager = PluginManager::new();
assert!(manager.plugin_for_extension("rs").is_none());
assert!(manager.plugin_by_id("rust").is_none());
assert_eq!(manager.plugins().len(), 0);
}
#[test]
fn test_register_plugin() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
assert!(manager.plugin_for_extension("rs").is_some());
assert!(manager.plugin_by_id("rust").is_some());
assert_eq!(manager.plugins().len(), 1);
}
#[test]
fn test_plugin_lookup_by_extension() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
let plugin = manager.plugin_for_extension("rs").unwrap();
assert_eq!(plugin.metadata().id, "rust");
assert_eq!(plugin.metadata().name, "Rust");
}
#[test]
fn test_plugin_lookup_by_extension_case_insensitive() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
assert!(manager.plugin_for_extension("RS").is_some());
}
#[test]
fn test_plugin_lookup_by_path_pulumi_stack() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new(
"pulumi",
"Pulumi",
&["pulumi.yaml"],
)));
let plugin = manager
.plugin_for_path(Path::new("Pulumi.dev.yaml"))
.expect("pulumi plugin should match");
assert_eq!(plugin.metadata().id, "pulumi");
}
#[test]
fn test_plugin_lookup_by_id() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
let plugin = manager.plugin_by_id("rust").unwrap();
assert_eq!(plugin.metadata().id, "rust");
assert_eq!(plugin.metadata().name, "Rust");
}
#[test]
fn test_plugin_lookup_miss() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
assert!(manager.plugin_for_extension("js").is_none());
assert!(manager.plugin_by_id("javascript").is_none());
}
#[test]
fn test_multiple_extensions() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new(
"typescript",
"TypeScript",
&["ts", "tsx"],
)));
assert!(manager.plugin_for_extension("ts").is_some());
assert!(manager.plugin_for_extension("tsx").is_some());
let plugin_ts = manager.plugin_for_extension("ts").unwrap();
let plugin_tsx = manager.plugin_for_extension("tsx").unwrap();
assert_eq!(plugin_ts.metadata().id, "typescript");
assert_eq!(plugin_tsx.metadata().id, "typescript");
}
#[test]
fn test_multiple_plugins() {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
manager.register_builtin(Box::new(MockPlugin::new(
"javascript",
"JavaScript",
&["js"],
)));
assert_eq!(manager.plugins().len(), 2);
assert!(manager.plugin_for_extension("rs").is_some());
assert!(manager.plugin_for_extension("js").is_some());
}
#[test]
fn test_with_plugins() {
let plugins: Vec<Box<dyn LanguagePlugin>> = vec![
Box::new(MockPlugin::new("rust", "Rust", &["rs"])),
Box::new(MockPlugin::new("javascript", "JavaScript", &["js"])),
];
let manager = PluginManager::with_plugins(plugins);
assert_eq!(manager.plugins().len(), 2);
assert!(manager.plugin_for_extension("rs").is_some());
assert!(manager.plugin_for_extension("js").is_some());
}
#[test]
fn test_plugin_state_hash_consistent() {
let manager1 = PluginManager::new();
let manager2 = PluginManager::new();
assert_eq!(manager1.plugin_state_hash(), manager2.plugin_state_hash());
}
#[test]
fn test_plugin_state_hash_changes() {
let mut manager = PluginManager::new();
let hash_before = manager.plugin_state_hash();
manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
let hash_after = manager.plugin_state_hash();
assert_ne!(hash_before, hash_after);
}
#[test]
fn test_plugin_manager_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<PluginManager>();
}
#[test]
fn test_selective_loading() {
let selective_plugins: Vec<Box<dyn LanguagePlugin>> = vec![Box::new(MockPlugin::new(
"test-lang",
"Test Language",
&["test"],
))];
let manager = PluginManager::with_plugins(selective_plugins);
assert_eq!(manager.plugins().len(), 1, "Expected exactly 1 plugin");
assert!(
manager.plugin_for_extension("test").is_some(),
"Test plugin should be available"
);
assert!(
manager.plugin_for_extension("rs").is_none(),
"Rust plugin should not be loaded (selective loading)"
);
assert!(
manager.plugin_for_extension("js").is_none(),
"JavaScript plugin should not be loaded (selective loading)"
);
}
#[test]
fn test_empty_has_no_plugins() {
let manager = PluginManager::empty();
assert_eq!(
manager.plugins().len(),
0,
"Empty manager should have 0 plugins"
);
assert!(
manager.plugin_for_extension("rs").is_none(),
"Empty manager should not have Rust plugin"
);
assert!(
manager.plugin_by_id("rust").is_none(),
"Empty manager should not have Rust plugin by ID"
);
let mut mutable_manager = PluginManager::empty();
mutable_manager.register_builtin(Box::new(MockPlugin::new("test", "Test", &["test"])));
assert_eq!(
mutable_manager.plugins().len(),
1,
"After manual registration, should have 1 plugin"
);
}
}