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 sqry_test_support::test_language()
380 }
381
382 fn parse_ast(
383 &self,
384 _content: &[u8],
385 ) -> Result<tree_sitter::Tree, super::super::error::ParseError> {
386 Err(super::super::error::ParseError::TreeSitterFailed)
387 }
388
389 fn extract_scopes(
390 &self,
391 _tree: &tree_sitter::Tree,
392 _content: &[u8],
393 _file_path: &Path,
394 ) -> Result<Vec<Scope>, super::super::error::ScopeError> {
395 Ok(Vec::new())
396 }
397 }
398
399 #[test]
400 fn test_empty_manager() {
401 let manager = PluginManager::new();
402 assert!(manager.plugin_for_extension("rs").is_none());
403 assert!(manager.plugin_by_id("rust").is_none());
404 assert_eq!(manager.plugins().len(), 0);
405 }
406
407 #[test]
408 fn test_register_plugin() {
409 let mut manager = PluginManager::new();
410 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
411
412 assert!(manager.plugin_for_extension("rs").is_some());
413 assert!(manager.plugin_by_id("rust").is_some());
414 assert_eq!(manager.plugins().len(), 1);
415 }
416
417 #[test]
418 fn test_plugin_lookup_by_extension() {
419 let mut manager = PluginManager::new();
420 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
421
422 let plugin = manager.plugin_for_extension("rs").unwrap();
423 assert_eq!(plugin.metadata().id, "rust");
424 assert_eq!(plugin.metadata().name, "Rust");
425 }
426
427 #[test]
428 fn test_plugin_lookup_by_extension_case_insensitive() {
429 let mut manager = PluginManager::new();
430 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
431
432 assert!(manager.plugin_for_extension("RS").is_some());
433 }
434
435 #[test]
436 fn test_plugin_lookup_by_path_pulumi_stack() {
437 let mut manager = PluginManager::new();
438 manager.register_builtin(Box::new(MockPlugin::new(
439 "pulumi",
440 "Pulumi",
441 &["pulumi.yaml"],
442 )));
443
444 let plugin = manager
445 .plugin_for_path(Path::new("Pulumi.dev.yaml"))
446 .expect("pulumi plugin should match");
447 assert_eq!(plugin.metadata().id, "pulumi");
448 }
449
450 #[test]
451 fn test_plugin_lookup_by_id() {
452 let mut manager = PluginManager::new();
453 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
454
455 let plugin = manager.plugin_by_id("rust").unwrap();
456 assert_eq!(plugin.metadata().id, "rust");
457 assert_eq!(plugin.metadata().name, "Rust");
458 }
459
460 #[test]
461 fn test_plugin_lookup_miss() {
462 let mut manager = PluginManager::new();
463 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
464
465 assert!(manager.plugin_for_extension("js").is_none());
466 assert!(manager.plugin_by_id("javascript").is_none());
467 }
468
469 #[test]
470 fn test_multiple_extensions() {
471 let mut manager = PluginManager::new();
472 manager.register_builtin(Box::new(MockPlugin::new(
473 "typescript",
474 "TypeScript",
475 &["ts", "tsx"],
476 )));
477
478 assert!(manager.plugin_for_extension("ts").is_some());
479 assert!(manager.plugin_for_extension("tsx").is_some());
480
481 let plugin_ts = manager.plugin_for_extension("ts").unwrap();
482 let plugin_tsx = manager.plugin_for_extension("tsx").unwrap();
483
484 assert_eq!(plugin_ts.metadata().id, "typescript");
486 assert_eq!(plugin_tsx.metadata().id, "typescript");
487 }
488
489 #[test]
490 fn test_multiple_plugins() {
491 let mut manager = PluginManager::new();
492 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
493 manager.register_builtin(Box::new(MockPlugin::new(
494 "javascript",
495 "JavaScript",
496 &["js"],
497 )));
498
499 assert_eq!(manager.plugins().len(), 2);
500 assert!(manager.plugin_for_extension("rs").is_some());
501 assert!(manager.plugin_for_extension("js").is_some());
502 }
503
504 #[test]
505 fn test_with_plugins() {
506 let plugins: Vec<Box<dyn LanguagePlugin>> = vec![
507 Box::new(MockPlugin::new("rust", "Rust", &["rs"])),
508 Box::new(MockPlugin::new("javascript", "JavaScript", &["js"])),
509 ];
510
511 let manager = PluginManager::with_plugins(plugins);
512
513 assert_eq!(manager.plugins().len(), 2);
514 assert!(manager.plugin_for_extension("rs").is_some());
515 assert!(manager.plugin_for_extension("js").is_some());
516 }
517
518 #[test]
519 fn test_plugin_state_hash_consistent() {
520 let manager1 = PluginManager::new();
521 let manager2 = PluginManager::new();
522 assert_eq!(manager1.plugin_state_hash(), manager2.plugin_state_hash());
523 }
524
525 #[test]
526 fn test_plugin_state_hash_changes() {
527 let mut manager = PluginManager::new();
528 let hash_before = manager.plugin_state_hash();
529
530 manager.register_builtin(Box::new(MockPlugin::new("rust", "Rust", &["rs"])));
531 let hash_after = manager.plugin_state_hash();
532
533 assert_ne!(hash_before, hash_after);
534 }
535
536 #[test]
537 fn test_plugin_manager_is_send_sync() {
538 fn assert_send_sync<T: Send + Sync>() {}
541 assert_send_sync::<PluginManager>();
542 }
543
544 #[test]
547 fn test_selective_loading() {
548 let selective_plugins: Vec<Box<dyn LanguagePlugin>> = vec![Box::new(MockPlugin::new(
550 "test-lang",
551 "Test Language",
552 &["test"],
553 ))];
554
555 let manager = PluginManager::with_plugins(selective_plugins);
556
557 assert_eq!(manager.plugins().len(), 1, "Expected exactly 1 plugin");
559
560 assert!(
562 manager.plugin_for_extension("test").is_some(),
563 "Test plugin should be available"
564 );
565
566 assert!(
568 manager.plugin_for_extension("rs").is_none(),
569 "Rust plugin should not be loaded (selective loading)"
570 );
571 assert!(
572 manager.plugin_for_extension("js").is_none(),
573 "JavaScript plugin should not be loaded (selective loading)"
574 );
575 }
576
577 #[test]
578 fn test_empty_has_no_plugins() {
579 let manager = PluginManager::empty();
581
582 assert_eq!(
583 manager.plugins().len(),
584 0,
585 "Empty manager should have 0 plugins"
586 );
587
588 assert!(
590 manager.plugin_for_extension("rs").is_none(),
591 "Empty manager should not have Rust plugin"
592 );
593 assert!(
594 manager.plugin_by_id("rust").is_none(),
595 "Empty manager should not have Rust plugin by ID"
596 );
597
598 let mut mutable_manager = PluginManager::empty();
600 mutable_manager.register_builtin(Box::new(MockPlugin::new("test", "Test", &["test"])));
601
602 assert_eq!(
603 mutable_manager.plugins().len(),
604 1,
605 "After manual registration, should have 1 plugin"
606 );
607 }
608}