1use super::types::LanguagePlugin;
4use std::collections::HashMap;
5use std::path::Path;
6
7pub struct PluginManager {
38 builtin_plugins: Vec<Box<dyn LanguagePlugin>>,
40
41 extension_cache: HashMap<String, usize>,
43
44 id_cache: HashMap<String, usize>,
46
47 plugin_state_hash: u64,
49}
50
51impl PluginManager {
52 #[must_use]
65 pub fn new() -> Self {
66 Self::with_plugins(Vec::new())
67 }
68
69 #[cfg(test)]
89 #[must_use]
90 pub fn empty() -> Self {
91 Self::with_plugins(Vec::new())
92 }
93
94 #[must_use]
115 pub fn with_plugins(plugins: Vec<Box<dyn LanguagePlugin>>) -> Self {
116 let plugin_state_hash = Self::compute_plugin_hash_static(&plugins);
117
118 let mut manager = Self {
119 builtin_plugins: plugins,
120 extension_cache: HashMap::new(),
121 id_cache: HashMap::new(),
122 plugin_state_hash,
123 };
124
125 manager.rebuild_caches();
127
128 manager
129 }
130
131 pub fn register_builtin(&mut self, plugin: Box<dyn LanguagePlugin>) {
146 let index = self.builtin_plugins.len();
147 self.builtin_plugins.push(plugin);
148
149 let plugin_ref = &self.builtin_plugins[index];
151 let metadata = plugin_ref.metadata();
152
153 self.id_cache.insert(metadata.id.to_string(), index);
155
156 for ext in plugin_ref.extensions() {
158 self.extension_cache.insert((*ext).to_string(), index);
159 }
160
161 self.update_plugin_hash();
163 }
164
165 #[must_use]
185 pub fn plugin_for_extension(&self, ext: &str) -> Option<&dyn LanguagePlugin> {
186 let ext = ext.to_ascii_lowercase();
187 self.extension_cache
188 .get(ext.as_str())
189 .and_then(|&index| self.builtin_plugins.get(index).map(|p| &**p))
190 }
191
192 #[must_use]
197 pub fn plugin_for_path(&self, path: &Path) -> Option<&dyn LanguagePlugin> {
198 let filename = path
199 .file_name()
200 .and_then(|name| name.to_str())
201 .map(|name| name.trim_start_matches('.').to_ascii_lowercase());
202
203 if let Some(name) = filename.as_deref()
204 && name.starts_with("pulumi.")
205 {
206 let ext = Path::new(name).extension().and_then(|e| e.to_str());
207 if matches!(ext, Some("yaml" | "yml" | "json"))
208 && let Some(plugin) = self.plugin_by_id("pulumi")
209 {
210 return Some(plugin);
211 }
212 }
213
214 if let Some(ext) = path.extension().and_then(|e| e.to_str())
215 && let Some(plugin) = self.plugin_for_extension(ext)
216 {
217 return Some(plugin);
218 }
219
220 filename
221 .as_deref()
222 .and_then(|name| self.plugin_for_extension(name))
223 }
224
225 #[must_use]
245 pub fn plugin_by_id(&self, id: &str) -> Option<&dyn LanguagePlugin> {
246 self.id_cache
247 .get(id)
248 .and_then(|&index| self.builtin_plugins.get(index).map(|p| &**p))
249 }
250
251 #[must_use]
268 pub fn plugins(&self) -> Vec<&dyn LanguagePlugin> {
269 self.builtin_plugins.iter().map(|p| &**p).collect()
270 }
271
272 fn compute_plugin_hash_static(plugins: &[Box<dyn LanguagePlugin>]) -> u64 {
274 use std::collections::hash_map::DefaultHasher;
275 use std::hash::Hasher;
276
277 let mut hasher = DefaultHasher::new();
278
279 let mut plugin_data: Vec<_> = plugins
281 .iter()
282 .map(|p| {
283 let meta = p.metadata();
284 (meta.id.to_string(), meta.version.to_string())
285 })
286 .collect();
287 plugin_data.sort_by(|a, b| a.0.cmp(&b.0));
288
289 for (id, version) in plugin_data {
290 hasher.write(id.as_bytes());
291 hasher.write(version.as_bytes());
292 }
293
294 hasher.finish()
295 }
296
297 #[must_use]
299 pub fn plugin_state_hash(&self) -> u64 {
300 self.plugin_state_hash
301 }
302
303 fn update_plugin_hash(&mut self) {
305 self.plugin_state_hash = Self::compute_plugin_hash_static(&self.builtin_plugins);
306 }
307
308 fn rebuild_caches(&mut self) {
313 self.extension_cache.clear();
314 self.id_cache.clear();
315
316 for (index, plugin) in self.builtin_plugins.iter().enumerate() {
317 let metadata = plugin.metadata();
318
319 self.id_cache.insert(metadata.id.to_string(), index);
321
322 for ext in plugin.extensions() {
324 self.extension_cache.insert((*ext).to_string(), index);
325 }
326 }
327 }
328}
329
330impl Default for PluginManager {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::ast::Scope;
340 use crate::plugin::LanguageMetadata;
341 use std::path::Path;
342
343 struct MockPlugin {
345 id: &'static str,
346 name: &'static str,
347 extensions: &'static [&'static str],
348 }
349
350 impl MockPlugin {
351 fn new(id: &'static str, name: &'static str, extensions: &'static [&'static str]) -> Self {
352 Self {
353 id,
354 name,
355 extensions,
356 }
357 }
358 }
359
360 impl LanguagePlugin for MockPlugin {
361 fn metadata(&self) -> LanguageMetadata {
362 LanguageMetadata {
363 id: self.id,
364 name: self.name,
365 version: "1.0.0",
366 author: "Test",
367 description: "Mock plugin for testing",
368 tree_sitter_version: "0.24",
369 }
370 }
371
372 fn extensions(&self) -> &'static [&'static str] {
373 self.extensions
374 }
375
376 fn language(&self) -> tree_sitter::Language {
377 tree_sitter_rust::LANGUAGE.into()
381 }
382
383 fn parse_ast(
384 &self,
385 _content: &[u8],
386 ) -> Result<tree_sitter::Tree, super::super::error::ParseError> {
387 Err(super::super::error::ParseError::TreeSitterFailed)
388 }
389
390 fn extract_scopes(
391 &self,
392 _tree: &tree_sitter::Tree,
393 _content: &[u8],
394 _file_path: &Path,
395 ) -> Result<Vec<Scope>, super::super::error::ScopeError> {
396 Ok(Vec::new())
397 }
398 }
399
400 #[test]
401 fn test_empty_manager() {
402 let manager = PluginManager::new();
403 assert!(manager.plugin_for_extension("rs").is_none());
404 assert!(manager.plugin_by_id("rust").is_none());
405 assert_eq!(manager.plugins().len(), 0);
406 }
407
408 #[test]
409 fn test_register_plugin() {
410 let mut manager = PluginManager::new();
411 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
412
413 assert!(manager.plugin_for_extension("rs").is_some());
414 assert!(manager.plugin_by_id("rust").is_some());
415 assert_eq!(manager.plugins().len(), 1);
416 }
417
418 #[test]
419 fn test_plugin_lookup_by_extension() {
420 let mut manager = PluginManager::new();
421 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
422
423 let plugin = manager.plugin_for_extension("rs").unwrap();
424 assert_eq!(plugin.metadata().id, "rust");
425 assert_eq!(plugin.metadata().name, "Rust");
426 }
427
428 #[test]
429 fn test_plugin_lookup_by_extension_case_insensitive() {
430 let mut manager = PluginManager::new();
431 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
432
433 assert!(manager.plugin_for_extension("RS").is_some());
434 }
435
436 #[test]
437 fn test_plugin_lookup_by_path_pulumi_stack() {
438 let mut manager = PluginManager::new();
439 manager.register_builtin(Box::new(MockPlugin::new(
440 "pulumi",
441 "Pulumi",
442 &["pulumi.yaml"],
443 )));
444
445 let plugin = manager
446 .plugin_for_path(Path::new("Pulumi.dev.yaml"))
447 .expect("pulumi plugin should match");
448 assert_eq!(plugin.metadata().id, "pulumi");
449 }
450
451 #[test]
452 fn test_plugin_lookup_by_id() {
453 let mut manager = PluginManager::new();
454 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
455
456 let plugin = manager.plugin_by_id("rust").unwrap();
457 assert_eq!(plugin.metadata().id, "rust");
458 assert_eq!(plugin.metadata().name, "Rust");
459 }
460
461 #[test]
462 fn test_plugin_lookup_miss() {
463 let mut manager = PluginManager::new();
464 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
465
466 assert!(manager.plugin_for_extension("js").is_none());
467 assert!(manager.plugin_by_id("javascript").is_none());
468 }
469
470 #[test]
471 fn test_multiple_extensions() {
472 let mut manager = PluginManager::new();
473 manager.register_builtin(Box::new(MockPlugin::new(
474 "typescript",
475 "TypeScript",
476 &["ts", "tsx"],
477 )));
478
479 assert!(manager.plugin_for_extension("ts").is_some());
480 assert!(manager.plugin_for_extension("tsx").is_some());
481
482 let plugin_ts = manager.plugin_for_extension("ts").unwrap();
483 let plugin_tsx = manager.plugin_for_extension("tsx").unwrap();
484
485 assert_eq!(plugin_ts.metadata().id, "typescript");
487 assert_eq!(plugin_tsx.metadata().id, "typescript");
488 }
489
490 #[test]
491 fn test_multiple_plugins() {
492 let mut manager = PluginManager::new();
493 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
494 manager.register_builtin(Box::new(MockPlugin::new(
495 "javascript",
496 "JavaScript",
497 &["js"],
498 )));
499
500 assert_eq!(manager.plugins().len(), 2);
501 assert!(manager.plugin_for_extension("rs").is_some());
502 assert!(manager.plugin_for_extension("js").is_some());
503 }
504
505 #[test]
506 fn test_with_plugins() {
507 let plugins: Vec<Box<dyn LanguagePlugin>> = vec![
508 Box::new(MockPlugin::new("rust", "Rust", &["rs"])),
509 Box::new(MockPlugin::new("javascript", "JavaScript", &["js"])),
510 ];
511
512 let manager = PluginManager::with_plugins(plugins);
513
514 assert_eq!(manager.plugins().len(), 2);
515 assert!(manager.plugin_for_extension("rs").is_some());
516 assert!(manager.plugin_for_extension("js").is_some());
517 }
518
519 #[test]
520 fn test_plugin_state_hash_consistent() {
521 let manager1 = PluginManager::new();
522 let manager2 = PluginManager::new();
523 assert_eq!(manager1.plugin_state_hash(), manager2.plugin_state_hash());
524 }
525
526 #[test]
527 fn test_plugin_state_hash_changes() {
528 let mut manager = PluginManager::new();
529 let hash_before = manager.plugin_state_hash();
530
531 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
532 let hash_after = manager.plugin_state_hash();
533
534 assert_ne!(hash_before, hash_after);
535 }
536
537 #[test]
538 fn test_plugin_manager_is_send_sync() {
539 fn assert_send_sync<T: Send + Sync>() {}
542 assert_send_sync::<PluginManager>();
543 }
544
545 #[test]
548 fn test_selective_loading() {
549 let selective_plugins: Vec<Box<dyn LanguagePlugin>> = vec![Box::new(MockPlugin::new(
551 "test-lang",
552 "Test Language",
553 &["test"],
554 ))];
555
556 let manager = PluginManager::with_plugins(selective_plugins);
557
558 assert_eq!(manager.plugins().len(), 1, "Expected exactly 1 plugin");
560
561 assert!(
563 manager.plugin_for_extension("test").is_some(),
564 "Test plugin should be available"
565 );
566
567 assert!(
569 manager.plugin_for_extension("rs").is_none(),
570 "Rust plugin should not be loaded (selective loading)"
571 );
572 assert!(
573 manager.plugin_for_extension("js").is_none(),
574 "JavaScript plugin should not be loaded (selective loading)"
575 );
576 }
577
578 #[test]
579 fn test_empty_has_no_plugins() {
580 let manager = PluginManager::empty();
582
583 assert_eq!(
584 manager.plugins().len(),
585 0,
586 "Empty manager should have 0 plugins"
587 );
588
589 assert!(
591 manager.plugin_for_extension("rs").is_none(),
592 "Empty manager should not have Rust plugin"
593 );
594 assert!(
595 manager.plugin_by_id("rust").is_none(),
596 "Empty manager should not have Rust plugin by ID"
597 );
598
599 let mut mutable_manager = PluginManager::empty();
601 mutable_manager.register_builtin(Box::new(MockPlugin::new("test", "Test", &["test"])));
602
603 assert_eq!(
604 mutable_manager.plugins().len(),
605 1,
606 "After manual registration, should have 1 plugin"
607 );
608 }
609}