1use crate::input::commands::{get_all_commands, Command, CommandSource, Suggestion};
7use crate::input::fuzzy::fuzzy_match;
8use crate::input::keybindings::Action;
9use crate::input::keybindings::KeyContext;
10use std::sync::{Arc, RwLock};
11
12pub struct CommandRegistry {
17 builtin_commands: Vec<Command>,
19
20 plugin_commands: Arc<RwLock<Vec<Command>>>,
22
23 command_history: Vec<String>,
26}
27
28impl CommandRegistry {
29 const MAX_HISTORY_SIZE: usize = 50;
31
32 pub fn new() -> Self {
34 Self {
35 builtin_commands: get_all_commands(),
36 plugin_commands: Arc::new(RwLock::new(Vec::new())),
37 command_history: Vec::new(),
38 }
39 }
40
41 pub fn refresh_builtin_commands(&mut self) {
43 self.builtin_commands = get_all_commands();
44 }
45
46 pub fn is_terminal_bypass_action(&self, action: &crate::input::keybindings::Action) -> bool {
58 use crate::input::keybindings::Action;
59 let Action::PluginAction(target) = action else {
60 return false;
61 };
62 let plugins = self.plugin_commands.read().unwrap();
63 plugins.iter().any(|cmd| {
64 cmd.terminal_bypass
65 && matches!(&cmd.action, Action::PluginAction(name) if name == target)
66 })
67 }
68
69 pub fn record_usage(&mut self, command_name: &str) {
74 self.command_history.retain(|name| name != command_name);
76
77 self.command_history.insert(0, command_name.to_string());
79
80 if self.command_history.len() > Self::MAX_HISTORY_SIZE {
82 self.command_history.truncate(Self::MAX_HISTORY_SIZE);
83 }
84 }
85
86 fn history_position(&self, command_name: &str) -> Option<usize> {
89 self.command_history
90 .iter()
91 .position(|name| name == command_name)
92 }
93
94 pub fn register(&self, command: Command) {
102 tracing::debug!(
103 "CommandRegistry::register: name='{}', action={:?}",
104 command.name,
105 command.action
106 );
107 let mut commands = self.plugin_commands.write().unwrap();
108
109 commands.retain(|c| c.name != command.name);
111
112 commands.push(command);
114 tracing::debug!(
115 "CommandRegistry::register: plugin_commands now has {} items",
116 commands.len()
117 );
118 }
119
120 pub fn try_register(&self, command: Command) -> Result<(), (String, CommandSource)> {
126 let mut commands = self.plugin_commands.write().unwrap();
127
128 if let Some(existing) = commands.iter().find(|c| c.name == command.name) {
129 if existing.source == command.source {
131 commands.retain(|c| c.name != command.name);
132 commands.push(command);
133 return Ok(());
134 }
135 return Err((existing.name.clone(), existing.source.clone()));
136 }
137
138 commands.push(command);
139 Ok(())
140 }
141
142 pub fn unregister(&self, name: &str) {
144 let mut commands = self.plugin_commands.write().unwrap();
145 commands.retain(|c| c.name != name);
146 }
147
148 pub fn unregister_by_prefix(&self, prefix: &str) {
150 let mut commands = self.plugin_commands.write().unwrap();
151 commands.retain(|c| !c.name.starts_with(prefix));
152 }
153
154 pub fn unregister_by_plugin(&self, plugin_name: &str) {
156 let mut commands = self.plugin_commands.write().unwrap();
157 let before = commands.len();
158 commands.retain(|c| {
159 if let super::commands::CommandSource::Plugin(ref name) = c.source {
160 name != plugin_name
161 } else {
162 true
163 }
164 });
165 let removed = before - commands.len();
166 if removed > 0 {
167 tracing::debug!(
168 "Unregistered {} commands from plugin '{}'",
169 removed,
170 plugin_name
171 );
172 }
173 }
174
175 pub fn get_all(&self) -> Vec<Command> {
177 let mut all_commands = self.builtin_commands.clone();
178
179 let plugin_commands = self.plugin_commands.read().unwrap();
180 let plugin_count = plugin_commands.len();
181
182 let target_action =
184 crate::input::keybindings::Action::PluginAction("vi_mode_toggle".to_string());
185 let has_target = plugin_commands.iter().any(|c| c.action == target_action);
186 if has_target {
187 tracing::debug!("get_all: vi_mode_toggle found via comparison!");
188 } else if plugin_count > 0 {
189 tracing::debug!(
190 "get_all: {} plugin commands but vi_mode_toggle NOT found",
191 plugin_count
192 );
193 }
194
195 all_commands.extend(plugin_commands.iter().cloned());
196
197 tracing::trace!(
198 "CommandRegistry::get_all: {} builtin + {} plugin = {} total",
199 self.builtin_commands.len(),
200 plugin_count,
201 all_commands.len()
202 );
203 all_commands
204 }
205
206 #[allow(clippy::too_many_arguments)]
216 pub fn filter(
217 &self,
218 query: &str,
219 current_context: KeyContext,
220 keybinding_resolver: &crate::input::keybindings::KeybindingResolver,
221 selection_active: bool,
222 active_custom_contexts: &std::collections::HashSet<String>,
223 active_buffer_mode: Option<&str>,
224 has_lsp_config: bool,
225 ) -> Vec<Suggestion> {
226 let commands = self.get_all();
227
228 let is_visible = |cmd: &Command| -> bool {
234 cmd.custom_contexts.is_empty()
235 || cmd.custom_contexts.iter().all(|ctx| {
236 active_custom_contexts.contains(ctx)
237 || active_buffer_mode.is_some_and(|mode| mode == ctx)
238 })
239 };
240
241 let is_available = |cmd: &Command| -> bool {
243 if cmd.contexts.contains(&KeyContext::Global) {
245 return true;
246 }
247
248 cmd.contexts.is_empty() || cmd.contexts.contains(¤t_context)
250 };
251
252 let current_context_ref = ¤t_context;
254 let make_suggestion =
255 |cmd: &Command, score: i32, localized_name: String, localized_desc: String| {
256 let mut available = is_available(cmd);
257 if cmd.action == Action::FindInSelection && !selection_active {
258 available = false;
259 }
260 if !has_lsp_config
262 && matches!(cmd.action, Action::LspRestart | Action::LspToggleForBuffer)
263 {
264 available = false;
265 }
266 let keybinding = keybinding_resolver
267 .get_keybinding_for_action(&cmd.action, current_context_ref.clone());
268 let history_pos = self.history_position(&cmd.name);
269
270 let suggestion = Suggestion::new(localized_name)
271 .with_description(localized_desc)
272 .set_disabled(!available)
273 .with_keybinding(keybinding)
274 .with_source(Some(cmd.source.clone()));
275 (suggestion, history_pos, score)
276 };
277
278 let mut suggestions: Vec<(Suggestion, Option<usize>, i32, u8)> = commands
282 .iter()
283 .filter(|cmd| is_visible(cmd))
284 .filter_map(|cmd| {
285 let localized_name = cmd.get_localized_name();
286 let name_result = fuzzy_match(query, &localized_name);
287 if name_result.matched {
288 let localized_desc = cmd.get_localized_description();
289 let (suggestion, hist, score) =
290 make_suggestion(cmd, name_result.score, localized_name, localized_desc);
291 Some((suggestion, hist, score, 0))
292 } else if !query.is_empty() {
293 let localized_desc = cmd.get_localized_description();
294 let desc_result = fuzzy_match(query, &localized_desc);
295 if desc_result.matched {
296 let (suggestion, hist, score) =
297 make_suggestion(cmd, desc_result.score, localized_name, localized_desc);
298 Some((suggestion, hist, score, 1))
299 } else {
300 None
301 }
302 } else {
303 None
304 }
305 })
306 .collect();
307
308 let has_query = !query.is_empty();
314 suggestions.sort_by(
315 |(a, a_hist, a_score, a_kind), (b, b_hist, b_score, b_kind)| {
316 match a.disabled.cmp(&b.disabled) {
318 std::cmp::Ordering::Equal => {}
319 other => return other,
320 }
321
322 if has_query {
323 match a_kind.cmp(b_kind) {
325 std::cmp::Ordering::Equal => {}
326 other => return other,
327 }
328
329 match b_score.cmp(a_score) {
331 std::cmp::Ordering::Equal => {}
332 other => return other,
333 }
334 }
335
336 match (a_hist, b_hist) {
338 (Some(a_pos), Some(b_pos)) => a_pos.cmp(b_pos),
339 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater,
341 (None, None) => a.text.cmp(&b.text), }
343 },
344 );
345
346 suggestions.into_iter().map(|(s, _, _, _)| s).collect()
348 }
349
350 pub fn plugin_command_count(&self) -> usize {
352 self.plugin_commands.read().unwrap().len()
353 }
354
355 pub fn total_command_count(&self) -> usize {
357 self.builtin_commands.len() + self.plugin_command_count()
358 }
359
360 pub fn find_by_name(&self, name: &str) -> Option<Command> {
362 {
364 let plugin_commands = self.plugin_commands.read().unwrap();
365 if let Some(cmd) = plugin_commands.iter().find(|c| c.name == name) {
366 return Some(cmd.clone());
367 }
368 }
369
370 self.builtin_commands
372 .iter()
373 .find(|c| c.name == name)
374 .cloned()
375 }
376}
377
378impl Default for CommandRegistry {
379 fn default() -> Self {
380 Self::new()
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::input::commands::CommandSource;
388 use crate::input::keybindings::Action;
389
390 #[test]
391 fn test_command_registry_creation() {
392 let registry = CommandRegistry::new();
393 assert!(registry.total_command_count() > 0); assert_eq!(registry.plugin_command_count(), 0); }
396
397 #[test]
398 fn test_register_command() {
399 let registry = CommandRegistry::new();
400
401 let custom_command = Command {
402 name: "Test Command".to_string(),
403 description: "A test command".to_string(),
404 action: Action::None,
405 contexts: vec![],
406 custom_contexts: vec![],
407 source: CommandSource::Builtin,
408 terminal_bypass: false,
409 };
410
411 registry.register(custom_command.clone());
412 assert_eq!(registry.plugin_command_count(), 1);
413
414 let found = registry.find_by_name("Test Command");
415 assert!(found.is_some());
416 assert_eq!(found.unwrap().description, "A test command");
417 }
418
419 #[test]
420 fn test_unregister_command() {
421 let registry = CommandRegistry::new();
422
423 let custom_command = Command {
424 name: "Test Command".to_string(),
425 description: "A test command".to_string(),
426 action: Action::None,
427 contexts: vec![],
428 custom_contexts: vec![],
429 source: CommandSource::Builtin,
430 terminal_bypass: false,
431 };
432
433 registry.register(custom_command);
434 assert_eq!(registry.plugin_command_count(), 1);
435
436 registry.unregister("Test Command");
437 assert_eq!(registry.plugin_command_count(), 0);
438 }
439
440 #[test]
441 fn test_register_replaces_existing() {
442 let registry = CommandRegistry::new();
443
444 let command1 = Command {
445 name: "Test Command".to_string(),
446 description: "First version".to_string(),
447 action: Action::None,
448 contexts: vec![],
449 custom_contexts: vec![],
450 source: CommandSource::Builtin,
451 terminal_bypass: false,
452 };
453
454 let command2 = Command {
455 name: "Test Command".to_string(),
456 description: "Second version".to_string(),
457 action: Action::None,
458 contexts: vec![],
459 custom_contexts: vec![],
460 source: CommandSource::Builtin,
461 terminal_bypass: false,
462 };
463
464 registry.register(command1);
465 assert_eq!(registry.plugin_command_count(), 1);
466
467 registry.register(command2);
468 assert_eq!(registry.plugin_command_count(), 1); let found = registry.find_by_name("Test Command").unwrap();
471 assert_eq!(found.description, "Second version");
472 }
473
474 #[test]
475 fn test_unregister_by_prefix() {
476 let registry = CommandRegistry::new();
477
478 registry.register(Command {
479 name: "Plugin A: Command 1".to_string(),
480 description: "".to_string(),
481 action: Action::None,
482 contexts: vec![],
483 custom_contexts: vec![],
484 source: CommandSource::Builtin,
485 terminal_bypass: false,
486 });
487
488 registry.register(Command {
489 name: "Plugin A: Command 2".to_string(),
490 description: "".to_string(),
491 action: Action::None,
492 contexts: vec![],
493 custom_contexts: vec![],
494 source: CommandSource::Builtin,
495 terminal_bypass: false,
496 });
497
498 registry.register(Command {
499 name: "Plugin B: Command".to_string(),
500 description: "".to_string(),
501 action: Action::None,
502 contexts: vec![],
503 custom_contexts: vec![],
504 source: CommandSource::Builtin,
505 terminal_bypass: false,
506 });
507
508 assert_eq!(registry.plugin_command_count(), 3);
509
510 registry.unregister_by_prefix("Plugin A:");
511 assert_eq!(registry.plugin_command_count(), 1);
512
513 let remaining = registry.find_by_name("Plugin B: Command");
514 assert!(remaining.is_some());
515 }
516
517 #[test]
518 fn test_filter_commands() {
519 use crate::config::Config;
520 use crate::input::keybindings::KeybindingResolver;
521
522 let registry = CommandRegistry::new();
523 let config = Config::default();
524 let keybindings = KeybindingResolver::new(&config);
525
526 registry.register(Command {
527 name: "Test Save".to_string(),
528 description: "Test save command".to_string(),
529 action: Action::None,
530 contexts: vec![KeyContext::Normal],
531 custom_contexts: vec![],
532 source: CommandSource::Builtin,
533 terminal_bypass: false,
534 });
535
536 let empty_contexts = std::collections::HashSet::new();
537 let results = registry.filter(
538 "save",
539 KeyContext::Normal,
540 &keybindings,
541 false,
542 &empty_contexts,
543 None,
544 true,
545 );
546 assert!(results.len() >= 2); let names: Vec<String> = results.iter().map(|s| s.text.clone()).collect();
550 assert!(names.iter().any(|n| n.contains("Save")));
551 }
552
553 #[test]
554 fn test_context_filtering() {
555 use crate::config::Config;
556 use crate::input::keybindings::KeybindingResolver;
557
558 let registry = CommandRegistry::new();
559 let config = Config::default();
560 let keybindings = KeybindingResolver::new(&config);
561
562 registry.register(Command {
563 name: "Normal Only".to_string(),
564 description: "Available only in normal context".to_string(),
565 action: Action::None,
566 contexts: vec![KeyContext::Normal],
567 custom_contexts: vec![],
568 source: CommandSource::Builtin,
569 terminal_bypass: false,
570 });
571
572 registry.register(Command {
573 name: "Popup Only".to_string(),
574 description: "Available only in popup context".to_string(),
575 action: Action::None,
576 contexts: vec![KeyContext::Popup],
577 custom_contexts: vec![],
578 source: CommandSource::Builtin,
579 terminal_bypass: false,
580 });
581
582 let empty_contexts = std::collections::HashSet::new();
584 let results = registry.filter(
585 "",
586 KeyContext::Normal,
587 &keybindings,
588 false,
589 &empty_contexts,
590 None,
591 true,
592 );
593 let popup_only = results.iter().find(|s| s.text == "Popup Only");
594 assert!(popup_only.is_some());
595 assert!(popup_only.unwrap().disabled);
596
597 let results = registry.filter(
599 "",
600 KeyContext::Popup,
601 &keybindings,
602 false,
603 &empty_contexts,
604 None,
605 true,
606 );
607 let normal_only = results.iter().find(|s| s.text == "Normal Only");
608 assert!(normal_only.is_some());
609 assert!(normal_only.unwrap().disabled);
610 }
611
612 #[test]
613 fn test_get_all_merges_commands() {
614 let registry = CommandRegistry::new();
615 let initial_count = registry.total_command_count();
616
617 registry.register(Command {
618 name: "Custom 1".to_string(),
619 description: "".to_string(),
620 action: Action::None,
621 contexts: vec![],
622 custom_contexts: vec![],
623 source: CommandSource::Builtin,
624 terminal_bypass: false,
625 });
626
627 registry.register(Command {
628 name: "Custom 2".to_string(),
629 description: "".to_string(),
630 action: Action::None,
631 contexts: vec![],
632 custom_contexts: vec![],
633 source: CommandSource::Builtin,
634 terminal_bypass: false,
635 });
636
637 let all = registry.get_all();
638 assert_eq!(all.len(), initial_count + 2);
639 }
640
641 #[test]
642 fn test_plugin_command_overrides_builtin() {
643 let registry = CommandRegistry::new();
644
645 let builtin = registry.find_by_name("Save File");
647 assert!(builtin.is_some());
648 let original_desc = builtin.unwrap().description;
649
650 registry.register(Command {
652 name: "Save File".to_string(),
653 description: "Custom save implementation".to_string(),
654 action: Action::None,
655 contexts: vec![],
656 custom_contexts: vec![],
657 source: CommandSource::Builtin,
658 terminal_bypass: false,
659 });
660
661 let custom = registry.find_by_name("Save File").unwrap();
663 assert_eq!(custom.description, "Custom save implementation");
664 assert_ne!(custom.description, original_desc);
665 }
666
667 #[test]
668 fn test_record_usage() {
669 let mut registry = CommandRegistry::new();
670
671 registry.record_usage("Save File");
672 assert_eq!(registry.history_position("Save File"), Some(0));
673
674 registry.record_usage("Open File");
675 assert_eq!(registry.history_position("Open File"), Some(0));
676 assert_eq!(registry.history_position("Save File"), Some(1));
677
678 registry.record_usage("Save File");
680 assert_eq!(registry.history_position("Save File"), Some(0));
681 assert_eq!(registry.history_position("Open File"), Some(1));
682 }
683
684 #[test]
685 fn test_history_sorting() {
686 use crate::config::Config;
687 use crate::input::keybindings::KeybindingResolver;
688
689 let mut registry = CommandRegistry::new();
690 let config = Config::default();
691 let keybindings = KeybindingResolver::new(&config);
692
693 registry.record_usage("Quit");
695 registry.record_usage("Save File");
696 registry.record_usage("Open File");
697
698 let empty_contexts = std::collections::HashSet::new();
700 let results = registry.filter(
701 "",
702 KeyContext::Normal,
703 &keybindings,
704 false,
705 &empty_contexts,
706 None,
707 true,
708 );
709
710 let open_pos = results.iter().position(|s| s.text == "Open File").unwrap();
712 let save_pos = results.iter().position(|s| s.text == "Save File").unwrap();
713 let quit_pos = results.iter().position(|s| s.text == "Quit").unwrap();
714
715 assert!(
717 open_pos < save_pos,
718 "Open File should come before Save File"
719 );
720 assert!(save_pos < quit_pos, "Save File should come before Quit");
721 }
722
723 #[test]
724 fn test_history_max_size() {
725 let mut registry = CommandRegistry::new();
726
727 for i in 0..60 {
729 registry.record_usage(&format!("Command {}", i));
730 }
731
732 assert_eq!(
734 registry.command_history.len(),
735 CommandRegistry::MAX_HISTORY_SIZE
736 );
737
738 assert_eq!(registry.history_position("Command 59"), Some(0));
740
741 assert_eq!(registry.history_position("Command 0"), None);
743 }
744
745 #[test]
746 fn test_unused_commands_alphabetical() {
747 use crate::config::Config;
748 use crate::input::keybindings::KeybindingResolver;
749
750 let mut registry = CommandRegistry::new();
751 let config = Config::default();
752 let keybindings = KeybindingResolver::new(&config);
753
754 registry.register(Command {
756 name: "Zebra Command".to_string(),
757 description: "".to_string(),
758 action: Action::None,
759 contexts: vec![],
760 custom_contexts: vec![],
761 source: CommandSource::Builtin,
762 terminal_bypass: false,
763 });
764
765 registry.register(Command {
766 name: "Alpha Command".to_string(),
767 description: "".to_string(),
768 action: Action::None,
769 contexts: vec![],
770 custom_contexts: vec![],
771 source: CommandSource::Builtin,
772 terminal_bypass: false,
773 });
774
775 registry.record_usage("Save File");
777
778 let empty_contexts = std::collections::HashSet::new();
779 let results = registry.filter(
780 "",
781 KeyContext::Normal,
782 &keybindings,
783 false,
784 &empty_contexts,
785 None,
786 true,
787 );
788
789 let save_pos = results.iter().position(|s| s.text == "Save File").unwrap();
790 let alpha_pos = results
791 .iter()
792 .position(|s| s.text == "Alpha Command")
793 .unwrap();
794 let zebra_pos = results
795 .iter()
796 .position(|s| s.text == "Zebra Command")
797 .unwrap();
798
799 assert!(
801 save_pos < alpha_pos,
802 "Save File should come before Alpha Command"
803 );
804 assert!(
806 alpha_pos < zebra_pos,
807 "Alpha Command should come before Zebra Command"
808 );
809 }
810
811 #[test]
812 fn test_required_commands_exist() {
813 crate::i18n::set_locale("en");
816 let registry = CommandRegistry::new();
817
818 let required_commands = [
819 ("Show Completions", Action::LspCompletion),
821 ("Go to Definition", Action::LspGotoDefinition),
822 ("Show Hover Info", Action::LspHover),
823 ("Find References", Action::LspReferences),
824 ("Show Manual", Action::ShowHelp),
826 ("Show Keyboard Shortcuts", Action::ShowKeyboardShortcuts),
827 ("Scroll Up", Action::ScrollUp),
829 ("Scroll Down", Action::ScrollDown),
830 ("Scroll Tabs Left", Action::ScrollTabsLeft),
831 ("Scroll Tabs Right", Action::ScrollTabsRight),
832 ("Smart Home", Action::SmartHome),
834 ("Delete Word Backward", Action::DeleteWordBackward),
836 ("Delete Word Forward", Action::DeleteWordForward),
837 ("Delete to End of Line", Action::DeleteToLineEnd),
838 ];
839
840 for (name, expected_action) in required_commands {
841 let cmd = registry.find_by_name(name);
842 assert!(
843 cmd.is_some(),
844 "Command '{}' should exist in command palette",
845 name
846 );
847 assert_eq!(
848 cmd.unwrap().action,
849 expected_action,
850 "Command '{}' should have action {:?}",
851 name,
852 expected_action
853 );
854 }
855 }
856
857 #[test]
858 fn test_try_register_first_writer_wins() {
859 let registry = CommandRegistry::new();
860
861 let cmd_a = Command {
862 name: "My Command".to_string(),
863 description: "From plugin A".to_string(),
864 action: Action::None,
865 contexts: vec![],
866 custom_contexts: vec![],
867 source: CommandSource::Plugin("plugin-a".to_string()),
868 terminal_bypass: false,
869 };
870
871 let cmd_b = Command {
872 name: "My Command".to_string(),
873 description: "From plugin B".to_string(),
874 action: Action::None,
875 contexts: vec![],
876 custom_contexts: vec![],
877 source: CommandSource::Plugin("plugin-b".to_string()),
878 terminal_bypass: false,
879 };
880
881 assert!(registry.try_register(cmd_a).is_ok());
883 assert_eq!(registry.plugin_command_count(), 1);
884
885 let result = registry.try_register(cmd_b);
887 assert!(result.is_err());
888 assert_eq!(registry.plugin_command_count(), 1);
889
890 let found = registry.find_by_name("My Command").unwrap();
892 assert_eq!(found.description, "From plugin A");
893 }
894
895 #[test]
896 fn test_try_register_same_plugin_allowed() {
897 let registry = CommandRegistry::new();
898
899 let cmd1 = Command {
900 name: "My Command".to_string(),
901 description: "Version 1".to_string(),
902 action: Action::None,
903 contexts: vec![],
904 custom_contexts: vec![],
905 source: CommandSource::Plugin("plugin-a".to_string()),
906 terminal_bypass: false,
907 };
908
909 let cmd2 = Command {
910 name: "My Command".to_string(),
911 description: "Version 2".to_string(),
912 action: Action::None,
913 contexts: vec![],
914 custom_contexts: vec![],
915 source: CommandSource::Plugin("plugin-a".to_string()),
916 terminal_bypass: false,
917 };
918
919 assert!(registry.try_register(cmd1).is_ok());
920 assert!(registry.try_register(cmd2).is_ok());
922 assert_eq!(registry.plugin_command_count(), 1);
923
924 let found = registry.find_by_name("My Command").unwrap();
925 assert_eq!(found.description, "Version 2");
926 }
927
928 #[test]
929 fn test_try_register_after_unregister() {
930 let registry = CommandRegistry::new();
931
932 let cmd_a = Command {
933 name: "My Command".to_string(),
934 description: "From plugin A".to_string(),
935 action: Action::None,
936 contexts: vec![],
937 custom_contexts: vec![],
938 source: CommandSource::Plugin("plugin-a".to_string()),
939 terminal_bypass: false,
940 };
941
942 let cmd_b = Command {
943 name: "My Command".to_string(),
944 description: "From plugin B".to_string(),
945 action: Action::None,
946 contexts: vec![],
947 custom_contexts: vec![],
948 source: CommandSource::Plugin("plugin-b".to_string()),
949 terminal_bypass: false,
950 };
951
952 assert!(registry.try_register(cmd_a).is_ok());
954
955 registry.unregister("My Command");
957 assert_eq!(registry.plugin_command_count(), 0);
958
959 assert!(registry.try_register(cmd_b).is_ok());
961 let found = registry.find_by_name("My Command").unwrap();
962 assert_eq!(found.description, "From plugin B");
963 }
964}