1use std::collections::HashMap;
42use std::sync::Arc;
43
44use lattice_core::BufferKind;
45use lattice_protocol::Event;
46use lattice_protocol::ids::BufferId;
47
48use crate::active::ActiveModes;
49use crate::capability::CapabilitySet;
50use crate::context::ModeContext;
51use crate::error::ModeActivationError;
52use crate::event::ModeEvent;
53use crate::guards::GuardStoreHandle;
54use crate::mode::{DynMode, Mode, ModeId, ModeKind};
55
56#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
58pub enum RegistrationError {
59 #[error("mode `{0}` is already registered")]
63 Duplicate(ModeId),
64 #[error("mode id `{0}` must end in `-mode` (naming convention)")]
70 MissingModeSuffix(ModeId),
71}
72
73#[derive(Clone)]
129pub struct ModeRegistry {
130 modes: HashMap<ModeId, Arc<dyn DynMode>>,
131 kind_index: HashMap<BufferKind, ModeId>,
132 lang_index: HashMap<String, ModeId>,
140 presents_index: HashMap<String, ModeId>,
143 enabled: std::collections::HashSet<ModeId>,
151}
152
153pub type ModeRegistryHandle = std::sync::Arc<arc_swap::ArcSwap<ModeRegistry>>;
161
162impl Default for ModeRegistry {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl std::fmt::Debug for ModeRegistry {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 f.debug_struct("ModeRegistry")
171 .field("count", &self.modes.len())
172 .finish_non_exhaustive()
173 }
174}
175
176impl ModeRegistry {
177 pub fn new() -> Self {
181 Self {
182 modes: HashMap::new(),
183 kind_index: HashMap::new(),
184 lang_index: HashMap::new(),
185 presents_index: HashMap::new(),
186 enabled: std::collections::HashSet::new(),
187 }
188 }
189
190 pub fn register<M: Mode>(&mut self, mode: M) -> Result<ModeId, RegistrationError> {
200 self.register_inner(mode, true)
201 }
202
203 pub fn register_available<M: Mode>(&mut self, mode: M) -> Result<ModeId, RegistrationError> {
210 self.register_inner(mode, false)
211 }
212
213 fn register_inner<M: Mode>(
214 &mut self,
215 mode: M,
216 enable: bool,
217 ) -> Result<ModeId, RegistrationError> {
218 let id = <M as Mode>::id(&mode);
219 if !id.as_str().ends_with("-mode") {
226 return Err(RegistrationError::MissingModeSuffix(id));
227 }
228 if self.modes.contains_key(&id) {
229 return Err(RegistrationError::Duplicate(id));
230 }
231 let target_kind = <M as Mode>::target_buffer_kind(&mode);
232 let target_lang = match <M as Mode>::kind(&mode) {
240 ModeKind::Major => <M as Mode>::target_language(&mode).map(str::to_owned),
241 ModeKind::Minor => {
242 if let Some(lang) = <M as Mode>::target_language(&mode) {
243 tracing::warn!(
244 mode = %id,
245 %lang,
246 "ModeRegistry: ignoring target_language on a MINOR mode; \
247 only a major can own a language"
248 );
249 }
250 None
251 }
252 };
253 let presented: Vec<String> = if matches!(<M as Mode>::kind(&mode), ModeKind::Major) {
257 <M as Mode>::presents_extensions(&mode)
258 .iter()
259 .map(|e| e.to_ascii_lowercase())
260 .collect()
261 } else {
262 Vec::new()
263 };
264 let arc: Arc<dyn DynMode> = Arc::new(mode);
265 self.modes.insert(id, arc);
266 for ext in presented {
267 match self.presents_index.entry(ext) {
268 std::collections::hash_map::Entry::Vacant(e) => {
269 e.insert(id);
270 }
271 std::collections::hash_map::Entry::Occupied(e) => {
272 tracing::warn!(
273 existing = %e.get(),
274 rejected = %id,
275 extension = %e.key(),
276 "ModeRegistry: ignoring duplicate presents_extensions \
277 claim; first registration wins"
278 );
279 }
280 }
281 }
282 if let Some(lang) = target_lang {
283 match self.lang_index.entry(lang) {
284 std::collections::hash_map::Entry::Vacant(e) => {
285 e.insert(id);
286 }
287 std::collections::hash_map::Entry::Occupied(e) => {
288 tracing::warn!(
289 existing = %e.get(),
290 rejected = %id,
291 lang = %e.key(),
292 "ModeRegistry: ignoring duplicate target_language \
293 claim; first registration wins"
294 );
295 }
296 }
297 }
298 if let Some(kind) = target_kind {
299 match self.kind_index.entry(kind) {
300 std::collections::hash_map::Entry::Vacant(e) => {
301 e.insert(id);
302 }
303 std::collections::hash_map::Entry::Occupied(e) => {
304 tracing::warn!(
305 existing = %e.get(),
306 rejected = %id,
307 ?kind,
308 "ModeRegistry: ignoring duplicate target_buffer_kind \
309 claim; first registration wins"
310 );
311 }
312 }
313 }
314 if enable {
315 self.enabled.insert(id);
316 }
317 Ok(id)
318 }
319
320 pub fn is_registered(&self, id: ModeId) -> bool {
322 self.modes.contains_key(&id)
323 }
324
325 pub fn set_minor_enabled(&mut self, id: ModeId, enabled: bool) {
332 if enabled {
333 self.enabled.insert(id);
334 } else {
335 self.enabled.remove(&id);
336 }
337 }
338
339 pub fn is_minor_enabled(&self, id: &ModeId) -> bool {
341 self.enabled.contains(id)
342 }
343
344 pub fn unregister(&mut self, id: ModeId) -> bool {
355 let removed = self.modes.remove(&id).is_some();
356 if removed {
357 self.kind_index.retain(|_, claimed| *claimed != id);
358 self.lang_index.retain(|_, claimed| *claimed != id);
362 self.presents_index.retain(|_, claimed| *claimed != id);
363 }
364 removed
365 }
366
367 pub fn get(&self, id: ModeId) -> Option<Arc<dyn DynMode>> {
369 self.modes.get(&id).cloned()
370 }
371
372 pub fn find_major_for_kind(&self, kind: BufferKind) -> Option<ModeId> {
380 self.kind_index.get(&kind).copied()
381 }
382
383 pub fn find_major_for_lang(&self, lang: &str) -> Option<ModeId> {
399 self.lang_index.get(lang).copied()
400 }
401
402 pub fn presenting_major_for_path(&self, path: &std::path::Path) -> Option<ModeId> {
409 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
410 self.presents_index.get(&ext).copied()
411 }
412
413 pub fn iter_meta(&self) -> impl Iterator<Item = (ModeId, ModeKind)> + '_ {
415 self.modes.iter().map(|(id, mode)| (*id, mode.kind()))
416 }
417
418 pub fn auto_activatable_minors(&self, major: &str, buffer_kind: BufferKind) -> Vec<ModeId> {
437 self.modes
438 .iter()
439 .filter(|(_, mode)| mode.kind() == ModeKind::Minor)
440 .filter(|(_, mode)| mode.activation_policy().admits(major, buffer_kind))
441 .filter(|(id, _)| self.enabled.contains(*id))
445 .map(|(id, _)| *id)
446 .collect()
447 }
448
449 pub fn iter(&self) -> impl Iterator<Item = (ModeId, Arc<dyn DynMode>)> + '_ {
461 self.modes.iter().map(|(id, mode)| (*id, Arc::clone(mode)))
462 }
463
464 pub fn len(&self) -> usize {
466 self.modes.len()
467 }
468
469 pub fn is_empty(&self) -> bool {
471 self.modes.is_empty()
472 }
473
474 #[allow(clippy::too_many_arguments)]
486 pub fn activate_major(
487 &self,
488 active: &mut ActiveModes,
489 guards: &GuardStoreHandle,
490 config: &Arc<lattice_config::ConfigRegistry>,
491 events: &Arc<lattice_runtime::EventBus>,
492 services: &Arc<crate::services::ServiceRegistry>,
493 buffer: BufferId,
494 mode: ModeId,
495 caps: CapabilitySet,
496 ) -> Result<(), ModeActivationError> {
497 let entry = self
498 .modes
499 .get(&mode)
500 .ok_or(ModeActivationError::NotRegistered(mode))?;
501 if entry.kind() != ModeKind::Major {
502 return Err(ModeActivationError::WrongKind { mode });
503 }
504 let missing = entry.required_capabilities() - caps;
505 if !missing.is_empty() {
506 return Err(ModeActivationError::MissingCapability { mode, missing });
507 }
508
509 if let Some(prev_id) = active.major() {
512 events.publish(Event::MajorExiting {
513 buffer,
514 major: prev_id.as_str().to_string(),
515 });
516 let _ = guards.remove(buffer, prev_id);
517 active.set_major(None);
518 }
519
520 active.set_major(Some(mode));
524
525 let mut plan: Vec<CascadeStep> = vec![CascadeStep {
531 entry: entry.clone(),
532 mode,
533 kind: ModeKind::Major,
534 epoch: guards.bump_epoch(buffer, mode),
535 }];
536 if let Err(e) =
537 self.record_implies_cascade(active, &mut plan, entry, mode, caps, buffer, guards)
538 {
539 active.set_major(None);
540 for step in plan.iter().skip(1) {
541 active.remove_minor(step.mode);
542 }
543 return Err(e);
544 }
545
546 self.spawn_cascade(
547 plan,
548 guards.clone(),
549 events.clone(),
550 config,
551 events,
552 services,
553 buffer,
554 );
555 Ok(())
556 }
557
558 #[allow(clippy::too_many_arguments)]
561 pub fn activate_minor(
562 &self,
563 active: &mut ActiveModes,
564 guards: &GuardStoreHandle,
565 config: &Arc<lattice_config::ConfigRegistry>,
566 events: &Arc<lattice_runtime::EventBus>,
567 services: &Arc<crate::services::ServiceRegistry>,
568 buffer: BufferId,
569 mode: ModeId,
570 caps: CapabilitySet,
571 ) -> Result<(), ModeActivationError> {
572 let mut plan: Vec<CascadeStep> = Vec::new();
573 self.validate_and_record_minor(active, &mut plan, buffer, mode, caps, guards)?;
574 if !plan.is_empty() {
577 self.spawn_cascade(
578 plan,
579 guards.clone(),
580 events.clone(),
581 config,
582 events,
583 services,
584 buffer,
585 );
586 }
587 Ok(())
588 }
589
590 #[allow(clippy::too_many_arguments)]
598 fn validate_and_record_minor(
599 &self,
600 active: &mut ActiveModes,
601 plan: &mut Vec<CascadeStep>,
602 buffer: BufferId,
603 mode: ModeId,
604 caps: CapabilitySet,
605 guards: &GuardStoreHandle,
606 ) -> Result<(), ModeActivationError> {
607 let entry = self
608 .modes
609 .get(&mode)
610 .ok_or(ModeActivationError::NotRegistered(mode))?;
611 if entry.kind() != ModeKind::Minor {
612 return Err(ModeActivationError::WrongKind { mode });
613 }
614 if active.has_minor(mode) {
615 return Ok(());
616 }
617 let missing = entry.required_capabilities() - caps;
618 if !missing.is_empty() {
619 return Err(ModeActivationError::MissingCapability { mode, missing });
620 }
621 for &c in entry.conflicts_with() {
623 if active.is_active(c) {
624 return Err(ModeActivationError::Conflict { mode, active: c });
625 }
626 }
627 if let Some(major) = active.major()
628 && let Some(major_entry) = self.modes.get(&major)
629 && major_entry.conflicts_with().contains(&mode)
630 {
631 return Err(ModeActivationError::Conflict {
632 mode,
633 active: major,
634 });
635 }
636 for &active_minor in active.minors() {
637 if let Some(minor_entry) = self.modes.get(&active_minor)
638 && minor_entry.conflicts_with().contains(&mode)
639 {
640 return Err(ModeActivationError::Conflict {
641 mode,
642 active: active_minor,
643 });
644 }
645 }
646
647 active.push_minor(mode);
648 plan.push(CascadeStep {
649 entry: entry.clone(),
650 mode,
651 kind: ModeKind::Minor,
652 epoch: guards.bump_epoch(buffer, mode),
653 });
654
655 if let Err(e) = self.record_implies_cascade(active, plan, entry, mode, caps, buffer, guards)
656 {
657 active.remove_minor(mode);
658 if let Some(pos) = plan.iter().position(|s| s.mode == mode) {
659 for step in plan.drain(pos..) {
660 if step.mode != mode {
661 active.remove_minor(step.mode);
662 }
663 }
664 }
665 return Err(e);
666 }
667
668 Ok(())
669 }
670
671 #[allow(clippy::too_many_arguments)]
675 fn record_implies_cascade(
676 &self,
677 active: &mut ActiveModes,
678 plan: &mut Vec<CascadeStep>,
679 entry: &Arc<dyn DynMode>,
680 mode: ModeId,
681 caps: CapabilitySet,
682 buffer: BufferId,
683 guards: &GuardStoreHandle,
684 ) -> Result<(), ModeActivationError> {
685 for &dep in entry.implies() {
686 if !self.is_registered(dep) {
687 return Err(ModeActivationError::UnregisteredDependency { mode, dep });
688 }
689 if active.has_minor(dep) {
690 continue;
691 }
692 self.validate_and_record_minor(active, plan, buffer, dep, caps, guards)?;
693 }
694 if entry.refresh_action().is_some() {
707 let shared = crate::RefreshableViewMode::mode_id();
708 if !active.has_minor(shared) && mode != shared {
709 if self.is_registered(shared) {
710 self.validate_and_record_minor(active, plan, buffer, shared, caps, guards)?;
711 } else {
712 tracing::debug!(
713 %mode,
714 "mode declares refresh_action but refreshable-view-mode is not registered; `gr` will not bind"
715 );
716 }
717 }
718 }
719 if entry.fold_toggle_action().is_some() {
726 let shared = crate::FoldableViewMode::mode_id();
727 if !active.has_minor(shared) && mode != shared {
728 if self.is_registered(shared) {
729 self.validate_and_record_minor(active, plan, buffer, shared, caps, guards)?;
730 } else {
731 tracing::debug!(
732 %mode,
733 "mode declares fold_toggle_action but foldable-view-mode is not registered; `<Tab>` will not bind"
734 );
735 }
736 }
737 }
738 Ok(())
739 }
740
741 pub fn deactivate_minor(
748 &self,
749 active: &mut ActiveModes,
750 guards: &GuardStoreHandle,
751 events: &Arc<lattice_runtime::EventBus>,
752 buffer: BufferId,
753 mode: ModeId,
754 ) -> Result<(), ModeActivationError> {
755 if !active.has_minor(mode) {
756 return Ok(());
757 }
758 let entry = self
759 .modes
760 .get(&mode)
761 .ok_or(ModeActivationError::NotRegistered(mode))?;
762 let implies: Vec<ModeId> = entry.implies().to_vec();
763 events.publish(Event::MinorDeactivated {
764 buffer,
765 minor: mode.as_str().to_string(),
766 });
767 let _ = guards.remove(buffer, mode);
771 active.remove_minor(mode);
772 for &dep in &implies {
775 if !active.has_minor(dep) {
776 continue;
777 }
778 self.deactivate_minor(active, guards, events, buffer, dep)?;
779 }
780 Ok(())
781 }
782
783 pub fn deactivate_major(
785 &self,
786 active: &mut ActiveModes,
787 guards: &GuardStoreHandle,
788 events: &Arc<lattice_runtime::EventBus>,
789 buffer: BufferId,
790 ) -> Result<(), ModeActivationError> {
791 let Some(mode) = active.major() else {
792 return Ok(());
793 };
794 let _ = self
795 .modes
796 .get(&mode)
797 .ok_or(ModeActivationError::NotRegistered(mode))?;
798 events.publish(Event::MajorExiting {
799 buffer,
800 major: mode.as_str().to_string(),
801 });
802 let _ = guards.remove(buffer, mode);
803 active.set_major(None);
804 Ok(())
805 }
806
807 #[allow(clippy::too_many_arguments)]
833 fn spawn_cascade(
834 &self,
835 plan: Vec<CascadeStep>,
836 guards: GuardStoreHandle,
837 events_for_task: Arc<lattice_runtime::EventBus>,
838 config: &Arc<lattice_config::ConfigRegistry>,
839 events: &Arc<lattice_runtime::EventBus>,
840 services: &Arc<crate::services::ServiceRegistry>,
841 buffer: BufferId,
842 ) {
843 let config = config.clone();
844 let events_ctx = events.clone();
845 let services = services.clone();
846 let cascade_fut = async move {
847 for (i, step) in plan.iter().enumerate() {
848 let ctx = ModeContext::new(
849 buffer,
850 step.mode,
851 config.clone(),
852 events_ctx.clone(),
853 services.clone(),
854 );
855 match step.entry.on_activate_dyn(ctx).await {
856 Ok(guard) => {
857 match guards.try_insert(buffer, step.mode, step.epoch, guard) {
865 Ok(()) => {
866 let name = step.mode.as_str().to_string();
872 match step.kind {
873 ModeKind::Major => {
874 events_for_task.publish(Event::MajorEntered {
875 buffer,
876 major: name,
877 })
878 }
879 ModeKind::Minor => {
880 events_for_task.publish(Event::MinorActivated {
881 buffer,
882 minor: name,
883 })
884 }
885 }
886 }
887 Err(stale_guard) => {
888 drop(stale_guard);
895 }
902 }
903 }
904 Err(err) => {
905 events_for_task
906 .publish_typed(ModeEvent::activation_failed(buffer, step.mode, &err));
907 let trigger = step.mode;
908 for remaining in &plan[i + 1..] {
909 events_for_task.publish_typed(ModeEvent::ModeActivationFailed {
910 buffer,
911 mode: remaining.mode,
912 reason: format!("cascade aborted by {trigger}"),
913 });
914 }
915 return;
916 }
917 }
918 }
919 };
920
921 let mut fut: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> =
926 Box::pin(cascade_fut);
927 let waker = std::task::Waker::noop();
928 let mut cx = std::task::Context::from_waker(waker);
929 match fut.as_mut().poll(&mut cx) {
930 std::task::Poll::Ready(()) => {
931 }
936 std::task::Poll::Pending => {
937 lattice_runtime::spawn_task(fut);
938 }
939 }
940 }
941}
942
943struct CascadeStep {
954 entry: Arc<dyn DynMode>,
955 mode: ModeId,
956 kind: ModeKind,
957 epoch: u64,
958}
959
960#[cfg(test)]
961mod tests {
962 #![allow(clippy::unwrap_used, clippy::panic)]
963 use super::*;
964 use crate::mode::LifecycleFuture;
965 use std::sync::Arc as StdArc;
966 use std::sync::atomic::{AtomicU32, Ordering};
967 use tokio::sync::mpsc::UnboundedReceiver;
968
969 struct MockMode {
971 id: ModeId,
972 kind: ModeKind,
973 required: CapabilitySet,
974 conflicts: Vec<ModeId>,
975 implies: Vec<ModeId>,
976 target_kind: Option<BufferKind>,
977 target_lang: Option<String>,
978 policy: crate::ActivationPolicy,
979 refresh: Option<&'static str>,
980 activate_calls: StdArc<AtomicU32>,
981 deactivate_calls: StdArc<AtomicU32>,
982 }
983
984 struct MockGuard {
985 deactivate_calls: StdArc<AtomicU32>,
986 }
987
988 impl Drop for MockGuard {
989 fn drop(&mut self) {
990 self.deactivate_calls.fetch_add(1, Ordering::SeqCst);
991 }
992 }
993
994 impl MockMode {
995 fn major(name: &str) -> Self {
996 Self {
997 id: ModeId::new(name),
998 kind: ModeKind::Major,
999 required: CapabilitySet::empty(),
1000 conflicts: Vec::new(),
1001 implies: Vec::new(),
1002 target_kind: None,
1003 target_lang: None,
1004 policy: crate::ActivationPolicy::Manual,
1005 refresh: None,
1006 activate_calls: StdArc::new(AtomicU32::new(0)),
1007 deactivate_calls: StdArc::new(AtomicU32::new(0)),
1008 }
1009 }
1010 fn targeting(mut self, kind: BufferKind) -> Self {
1011 self.target_kind = Some(kind);
1012 self
1013 }
1014 fn for_lang(mut self, lang: &str) -> Self {
1016 self.target_lang = Some(lang.to_string());
1017 self
1018 }
1019 fn minor(name: &str) -> Self {
1020 Self {
1021 id: ModeId::new(name),
1022 kind: ModeKind::Minor,
1023 required: CapabilitySet::empty(),
1024 conflicts: Vec::new(),
1025 implies: Vec::new(),
1026 target_kind: None,
1027 target_lang: None,
1028 policy: crate::ActivationPolicy::Manual,
1029 refresh: None,
1030 activate_calls: StdArc::new(AtomicU32::new(0)),
1031 deactivate_calls: StdArc::new(AtomicU32::new(0)),
1032 }
1033 }
1034 fn requires(mut self, caps: CapabilitySet) -> Self {
1035 self.required = caps;
1036 self
1037 }
1038 fn conflicting_with(mut self, other: ModeId) -> Self {
1039 self.conflicts.push(other);
1040 self
1041 }
1042 fn implying(mut self, other: ModeId) -> Self {
1043 self.implies.push(other);
1044 self
1045 }
1046 fn with_policy(mut self, policy: crate::ActivationPolicy) -> Self {
1047 self.policy = policy;
1048 self
1049 }
1050 fn refreshing(mut self, action: &'static str) -> Self {
1053 self.refresh = Some(action);
1054 self
1055 }
1056 }
1057
1058 impl Mode for MockMode {
1059 type Guard = MockGuard;
1060 fn id(&self) -> ModeId {
1061 self.id
1062 }
1063 fn kind(&self) -> ModeKind {
1064 self.kind
1065 }
1066 fn target_buffer_kind(&self) -> Option<BufferKind> {
1067 self.target_kind
1068 }
1069 fn target_language(&self) -> Option<&str> {
1070 self.target_lang.as_deref()
1071 }
1072 fn required_capabilities(&self) -> CapabilitySet {
1073 self.required
1074 }
1075 fn conflicts_with(&self) -> &[ModeId] {
1076 &self.conflicts
1077 }
1078 fn implies(&self) -> &[ModeId] {
1079 &self.implies
1080 }
1081 fn activation_policy(&self) -> crate::ActivationPolicy {
1082 self.policy.clone()
1083 }
1084 fn refresh_action(&self) -> Option<&'static str> {
1085 self.refresh
1086 }
1087 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
1088 self.activate_calls.fetch_add(1, Ordering::SeqCst);
1089 let deact = self.deactivate_calls.clone();
1090 Box::pin(async move {
1091 Ok(MockGuard {
1092 deactivate_calls: deact,
1093 })
1094 })
1095 }
1096 }
1097
1098 fn buf() -> BufferId {
1099 BufferId::new(1)
1100 }
1101
1102 fn cfg() -> Arc<lattice_config::ConfigRegistry> {
1103 Arc::new(lattice_config::ConfigRegistry::new())
1104 }
1105
1106 fn evts() -> Arc<lattice_runtime::EventBus> {
1107 Arc::new(lattice_runtime::EventBus::new())
1108 }
1109
1110 fn svcs() -> Arc<crate::services::ServiceRegistry> {
1111 Arc::new(crate::services::ServiceRegistry::new())
1112 }
1113
1114 fn subscribe_mode_events(bus: &lattice_runtime::EventBus) -> UnboundedReceiver<ModeEvent> {
1119 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1120 bus.subscribe_typed::<ModeEvent>(tx);
1121 rx
1122 }
1123
1124 async fn await_event(rx: &mut UnboundedReceiver<ModeEvent>) -> ModeEvent {
1126 rx.recv()
1127 .await
1128 .expect("bus channel should deliver the event")
1129 }
1130
1131 fn subscribe_lifecycle(
1136 bus: &lattice_runtime::EventBus,
1137 ) -> UnboundedReceiver<lattice_protocol::Event> {
1138 use lattice_protocol::EventKind;
1139 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1140 bus.subscribe(
1141 lattice_runtime::EventFilter::kinds(vec![
1142 EventKind::MajorEntered,
1143 EventKind::MajorExiting,
1144 EventKind::MinorActivated,
1145 EventKind::MinorDeactivated,
1146 ]),
1147 lattice_runtime::SubscriptionTarget::Channel(tx),
1148 );
1149 rx
1150 }
1151
1152 async fn await_lifecycle(
1155 rx: &mut UnboundedReceiver<lattice_protocol::Event>,
1156 ) -> lattice_protocol::Event {
1157 rx.recv()
1158 .await
1159 .expect("bus channel should deliver the lifecycle event")
1160 }
1161
1162 #[test]
1163 fn find_major_for_kind_returns_registered_mode() {
1164 let mut r = ModeRegistry::new();
1165 let id = r
1166 .register(MockMode::major("ft-mode").targeting(BufferKind::FileTree))
1167 .unwrap();
1168 assert_eq!(r.find_major_for_kind(BufferKind::FileTree), Some(id));
1169 }
1170
1171 #[test]
1172 fn activation_policy_admits_matches_expected_majors() {
1173 use crate::ActivationPolicy;
1174 let doc = BufferKind::Document;
1175 assert!(!ActivationPolicy::Manual.admits("rust-mode", doc));
1176 assert!(ActivationPolicy::Global.admits("rust-mode", doc));
1178 assert!(ActivationPolicy::Global.admits("python-mode", doc));
1179 assert!(
1180 !ActivationPolicy::Global.admits("help-mode", BufferKind::Help),
1181 "Global is scoped to document buffers — never fires in synthetic UI buffers"
1182 );
1183 let allow = ActivationPolicy::Majors(vec![ModeId::new("rust-mode")]);
1186 assert!(allow.admits("rust-mode", doc));
1187 assert!(!allow.admits("python-mode", doc));
1188 assert!(
1189 ActivationPolicy::Majors(vec![ModeId::new("help-mode")])
1190 .admits("help-mode", BufferKind::Help),
1191 "an explicit major allowlist activates inside that major regardless of kind"
1192 );
1193 assert!(!ActivationPolicy::Majors(vec![]).admits("rust-mode", doc));
1195 for kind in [
1198 BufferKind::Document,
1199 BufferKind::Messages,
1200 BufferKind::Help,
1201 BufferKind::FileTree,
1202 BufferKind::Oil,
1203 BufferKind::Terminal,
1204 ] {
1205 assert!(
1206 ActivationPolicy::Universal.admits("any-mode", kind),
1207 "Universal must admit {kind:?}"
1208 );
1209 }
1210 }
1211
1212 #[test]
1213 fn register_rejects_mode_id_without_mode_suffix() {
1214 let mut r = ModeRegistry::new();
1218 let err = r.register(MockMode::minor("emacs-keys")).unwrap_err();
1219 assert_eq!(
1220 err,
1221 RegistrationError::MissingModeSuffix(ModeId::new("emacs-keys"))
1222 );
1223 assert!(r.register(MockMode::minor("emacs-keys-mode")).is_ok());
1225 }
1226
1227 #[test]
1228 fn auto_activatable_minors_filters_by_policy_and_kind() {
1229 use crate::ActivationPolicy;
1230 let mut r = ModeRegistry::new();
1231 r.register(MockMode::major("rust-mode").with_policy(ActivationPolicy::Global))
1234 .unwrap();
1235 r.register(MockMode::minor("manual-mode")).unwrap();
1237 let global = r
1239 .register(MockMode::minor("global-mode").with_policy(ActivationPolicy::Global))
1240 .unwrap();
1241 let rusty = r
1243 .register(
1244 MockMode::minor("rusty-mode")
1245 .with_policy(ActivationPolicy::Majors(vec![ModeId::new("rust-mode")])),
1246 )
1247 .unwrap();
1248
1249 let mut for_rust = r.auto_activatable_minors("rust-mode", BufferKind::Document);
1250 for_rust.sort();
1251 let mut expected = vec![global, rusty];
1252 expected.sort();
1253 assert_eq!(
1254 for_rust, expected,
1255 "global + allowlisted minors fire for a rust document"
1256 );
1257
1258 assert_eq!(
1260 r.auto_activatable_minors("python-mode", BufferKind::Document),
1261 vec![global],
1262 "only the global minor fires for a python document"
1263 );
1264
1265 assert!(
1269 r.auto_activatable_minors("help-mode", BufferKind::Help)
1270 .is_empty(),
1271 "Global minors must not auto-activate in synthetic buffers"
1272 );
1273 }
1274
1275 #[test]
1276 fn plugin_minor_is_inert_until_enabled() {
1277 use crate::ActivationPolicy;
1278 let mut r = ModeRegistry::new();
1279
1280 let native = r
1282 .register(MockMode::minor("native-mode").with_policy(ActivationPolicy::Global))
1283 .unwrap();
1284 let plugin = r
1286 .register_available(
1287 MockMode::minor("plugin-mode").with_policy(ActivationPolicy::Global),
1288 )
1289 .unwrap();
1290
1291 assert!(
1292 r.is_registered(plugin),
1293 "plugin mode is available (registered)"
1294 );
1295 assert!(
1296 !r.is_minor_enabled(&plugin),
1297 "plugin mode is NOT enabled at registration"
1298 );
1299 assert!(r.is_minor_enabled(&native), "native mode is auto-enabled");
1300
1301 assert_eq!(
1303 r.auto_activatable_minors("rust-mode", BufferKind::Document),
1304 vec![native],
1305 "the plugin minor does not auto-activate until enabled"
1306 );
1307
1308 r.set_minor_enabled(plugin, true);
1310 let mut got = r.auto_activatable_minors("rust-mode", BufferKind::Document);
1311 got.sort();
1312 let mut expected = vec![native, plugin];
1313 expected.sort();
1314 assert_eq!(got, expected, "an enabled plugin minor auto-activates");
1315
1316 r.set_minor_enabled(plugin, false);
1318 assert_eq!(
1319 r.auto_activatable_minors("rust-mode", BufferKind::Document),
1320 vec![native],
1321 "disabling removes the plugin minor from activation"
1322 );
1323 }
1324
1325 #[test]
1328 fn find_major_for_lang_resolves_a_declared_claim() {
1329 let mut r = ModeRegistry::new();
1330 let org = r
1331 .register(MockMode::major("org-mode").for_lang("org"))
1332 .unwrap();
1333 assert_eq!(r.find_major_for_lang("org"), Some(org));
1334 }
1335
1336 #[test]
1337 fn find_major_for_lang_returns_none_when_unclaimed() {
1338 let mut r = ModeRegistry::new();
1339 r.register(MockMode::major("org-mode").for_lang("org"))
1340 .unwrap();
1341 assert_eq!(r.find_major_for_lang("rust"), None);
1344 assert_eq!(ModeRegistry::new().find_major_for_lang("org"), None);
1346 }
1347
1348 #[test]
1349 fn find_major_for_lang_keeps_first_registration_when_clobbered() {
1350 let mut r = ModeRegistry::new();
1354 let first = r
1355 .register(MockMode::major("org-a-mode").for_lang("org"))
1356 .unwrap();
1357 let _second = r
1358 .register(MockMode::major("org-b-mode").for_lang("org"))
1359 .unwrap();
1360 assert_eq!(r.find_major_for_lang("org"), Some(first));
1361 }
1362
1363 #[test]
1364 fn a_minor_claiming_a_language_is_not_indexed() {
1365 let mut r = ModeRegistry::new();
1370 r.register(MockMode::minor("org-todo-mode").for_lang("org"))
1371 .unwrap();
1372 assert_eq!(r.find_major_for_lang("org"), None);
1373 }
1374
1375 #[test]
1376 fn unregister_frees_the_language_claim() {
1377 let mut r = ModeRegistry::new();
1380 let org = r
1381 .register(MockMode::major("org-mode").for_lang("org"))
1382 .unwrap();
1383 assert_eq!(r.find_major_for_lang("org"), Some(org));
1384
1385 assert!(r.unregister(org));
1386 assert_eq!(r.find_major_for_lang("org"), None);
1387
1388 let reloaded = r
1391 .register(MockMode::major("org-mode").for_lang("org"))
1392 .unwrap();
1393 assert_eq!(r.find_major_for_lang("org"), Some(reloaded));
1394 }
1395
1396 #[test]
1397 fn kind_and_language_claims_are_independent() {
1398 let mut r = ModeRegistry::new();
1402 let both = r
1403 .register(
1404 MockMode::major("markdownish-mode")
1405 .targeting(BufferKind::Help)
1406 .for_lang("markdown"),
1407 )
1408 .unwrap();
1409 assert_eq!(r.find_major_for_kind(BufferKind::Help), Some(both));
1410 assert_eq!(r.find_major_for_lang("markdown"), Some(both));
1411
1412 assert!(r.unregister(both));
1413 assert_eq!(r.find_major_for_kind(BufferKind::Help), None);
1414 assert_eq!(r.find_major_for_lang("markdown"), None);
1415 }
1416
1417 #[test]
1418 fn find_major_for_kind_returns_none_when_unbound() {
1419 let mut r = ModeRegistry::new();
1420 r.register(MockMode::major("plain-mode")).unwrap();
1421 assert_eq!(r.find_major_for_kind(BufferKind::Oil), None);
1424 }
1425
1426 #[test]
1427 fn find_major_for_kind_keeps_first_registration_when_clobbered() {
1428 let mut r = ModeRegistry::new();
1432 let first = r
1433 .register(MockMode::major("oil-a-mode").targeting(BufferKind::Oil))
1434 .unwrap();
1435 let _second = r
1436 .register(MockMode::major("oil-b-mode").targeting(BufferKind::Oil))
1437 .unwrap();
1438 assert_eq!(r.find_major_for_kind(BufferKind::Oil), Some(first));
1439 }
1440
1441 #[test]
1442 fn find_major_for_kind_ignores_minor_target() {
1443 let mut r = ModeRegistry::new();
1450 r.register(MockMode::minor("a-minor-mode")).unwrap();
1451 assert_eq!(r.find_major_for_kind(BufferKind::Document), None);
1452 }
1453
1454 #[test]
1455 fn unregister_removes_mode_and_frees_its_kind_claim() {
1456 let mut r = ModeRegistry::new();
1457 let major = r
1458 .register(MockMode::major("rust-mode").targeting(BufferKind::Document))
1459 .unwrap();
1460 let minor = r.register(MockMode::minor("keep-mode")).unwrap();
1461 assert_eq!(r.find_major_for_kind(BufferKind::Document), Some(major));
1462
1463 assert!(r.unregister(major));
1466 assert!(!r.is_registered(major));
1467 assert!(r.get(major).is_none());
1468 assert_eq!(r.find_major_for_kind(BufferKind::Document), None);
1469 assert!(r.is_registered(minor));
1470
1471 assert!(!r.unregister(major));
1475
1476 let reloaded = r
1479 .register(MockMode::major("rust-mode").targeting(BufferKind::Document))
1480 .unwrap();
1481 assert_eq!(reloaded, major, "interned id is stable across reload");
1482 assert_eq!(r.find_major_for_kind(BufferKind::Document), Some(reloaded));
1483 }
1484
1485 #[tokio::test]
1486 async fn register_and_lookup() {
1487 let mut r = ModeRegistry::new();
1488 let id = r.register(MockMode::major("rust-mode")).unwrap();
1489 assert!(r.is_registered(id));
1490 assert_eq!(r.get(id).unwrap().kind(), ModeKind::Major);
1491 assert_eq!(r.len(), 1);
1492 }
1493
1494 #[tokio::test]
1495 async fn duplicate_registration_fails() {
1496 let mut r = ModeRegistry::new();
1497 r.register(MockMode::major("rust-mode")).unwrap();
1498 let err = r.register(MockMode::major("rust-mode")).unwrap_err();
1499 assert!(matches!(err, RegistrationError::Duplicate(_)));
1500 }
1501
1502 #[tokio::test]
1503 async fn activate_unregistered_major_fails() {
1504 let r = ModeRegistry::new();
1505 let mut a = ActiveModes::new();
1506 let g = GuardStoreHandle::new();
1507 let err = r
1508 .activate_major(
1509 &mut a,
1510 &g,
1511 &cfg(),
1512 &evts(),
1513 &svcs(),
1514 buf(),
1515 ModeId::new("ghost-mode"),
1516 CapabilitySet::empty(),
1517 )
1518 .unwrap_err();
1519 assert!(matches!(err, ModeActivationError::NotRegistered(_)));
1520 }
1521
1522 #[tokio::test]
1523 async fn activate_wrong_kind_fails() {
1524 let mut r = ModeRegistry::new();
1525 let id = r.register(MockMode::minor("read-only-mode")).unwrap();
1526 let mut a = ActiveModes::new();
1527 let g = GuardStoreHandle::new();
1528 let err = r
1529 .activate_major(
1530 &mut a,
1531 &g,
1532 &cfg(),
1533 &evts(),
1534 &svcs(),
1535 buf(),
1536 id,
1537 CapabilitySet::empty(),
1538 )
1539 .unwrap_err();
1540 assert!(matches!(err, ModeActivationError::WrongKind { .. }));
1541 }
1542
1543 #[tokio::test]
1544 async fn missing_capability_blocks_activation() {
1545 let mut r = ModeRegistry::new();
1546 let id = r
1547 .register(MockMode::minor("lsp-mode").requires(CapabilitySet::LSP))
1548 .unwrap();
1549 let mut a = ActiveModes::new();
1550 let g = GuardStoreHandle::new();
1551 let err = r
1552 .activate_minor(
1553 &mut a,
1554 &g,
1555 &cfg(),
1556 &evts(),
1557 &svcs(),
1558 buf(),
1559 id,
1560 CapabilitySet::empty(),
1561 )
1562 .unwrap_err();
1563 match err {
1564 ModeActivationError::MissingCapability { mode, missing } => {
1565 assert_eq!(mode, id);
1566 assert_eq!(missing, CapabilitySet::LSP);
1567 }
1568 _ => panic!("expected MissingCapability"),
1569 }
1570 }
1571
1572 #[tokio::test]
1573 async fn major_activation_publishes_entered_and_stashes_guard() {
1574 let mock = MockMode::major("rust-mode");
1575 let act = mock.activate_calls.clone();
1576 let mut r = ModeRegistry::new();
1577 let id = r.register(mock).unwrap();
1578 let mut a = ActiveModes::new();
1579 let g = GuardStoreHandle::new();
1580 let bus = evts();
1581 let mut rx = subscribe_lifecycle(&bus);
1582 r.activate_major(
1583 &mut a,
1584 &g,
1585 &cfg(),
1586 &bus,
1587 &svcs(),
1588 buf(),
1589 id,
1590 CapabilitySet::empty(),
1591 )
1592 .unwrap();
1593 assert_eq!(a.major(), Some(id));
1595 let evt = await_lifecycle(&mut rx).await;
1598 assert!(matches!(evt, Event::MajorEntered { major, .. } if major == id.as_str()));
1599 assert_eq!(act.load(Ordering::SeqCst), 1);
1600 assert!(g.contains(buf(), id), "Guard stashed by spawned task");
1601 }
1602
1603 #[tokio::test]
1604 async fn major_swap_publishes_exiting_then_entered() {
1605 let prev = MockMode::major("text-mode");
1606 let new = MockMode::major("rust-mode");
1607 let prev_deact = prev.deactivate_calls.clone();
1608 let new_act = new.activate_calls.clone();
1609 let mut r = ModeRegistry::new();
1610 let prev_id = r.register(prev).unwrap();
1611 let new_id = r.register(new).unwrap();
1612 let mut a = ActiveModes::new();
1613 let g = GuardStoreHandle::new();
1614 let bus = evts();
1615 let mut rx = subscribe_lifecycle(&bus);
1616 r.activate_major(
1618 &mut a,
1619 &g,
1620 &cfg(),
1621 &bus,
1622 &svcs(),
1623 buf(),
1624 prev_id,
1625 CapabilitySet::empty(),
1626 )
1627 .unwrap();
1628 let evt = await_lifecycle(&mut rx).await;
1629 assert!(matches!(evt, Event::MajorEntered { major, .. } if major == prev_id.as_str()));
1630 r.activate_major(
1632 &mut a,
1633 &g,
1634 &cfg(),
1635 &bus,
1636 &svcs(),
1637 buf(),
1638 new_id,
1639 CapabilitySet::empty(),
1640 )
1641 .unwrap();
1642 let exiting = await_lifecycle(&mut rx).await;
1645 assert!(matches!(exiting, Event::MajorExiting { major, .. } if major == prev_id.as_str()));
1646 let entered = await_lifecycle(&mut rx).await;
1647 assert!(matches!(entered, Event::MajorEntered { major, .. } if major == new_id.as_str()));
1648 assert_eq!(prev_deact.load(Ordering::SeqCst), 1);
1650 assert_eq!(new_act.load(Ordering::SeqCst), 1);
1651 assert_eq!(a.major(), Some(new_id));
1652 assert!(g.contains(buf(), new_id));
1653 assert!(!g.contains(buf(), prev_id));
1654 }
1655
1656 #[tokio::test]
1657 async fn major_reload_drops_then_reactivates() {
1658 let mock = MockMode::major("rust-mode");
1659 let act = mock.activate_calls.clone();
1660 let deact = mock.deactivate_calls.clone();
1661 let mut r = ModeRegistry::new();
1662 let id = r.register(mock).unwrap();
1663 let mut a = ActiveModes::new();
1664 let g = GuardStoreHandle::new();
1665 let bus = evts();
1666 let mut rx = subscribe_lifecycle(&bus);
1667 r.activate_major(
1668 &mut a,
1669 &g,
1670 &cfg(),
1671 &bus,
1672 &svcs(),
1673 buf(),
1674 id,
1675 CapabilitySet::empty(),
1676 )
1677 .unwrap();
1678 let _entered = await_lifecycle(&mut rx).await;
1679 r.activate_major(
1680 &mut a,
1681 &g,
1682 &cfg(),
1683 &bus,
1684 &svcs(),
1685 buf(),
1686 id,
1687 CapabilitySet::empty(),
1688 )
1689 .unwrap();
1690 let _exiting = await_lifecycle(&mut rx).await;
1691 let _re_entered = await_lifecycle(&mut rx).await;
1692 assert_eq!(act.load(Ordering::SeqCst), 2);
1693 assert_eq!(deact.load(Ordering::SeqCst), 1);
1694 }
1695
1696 #[tokio::test]
1697 async fn minor_activation_appends_in_order() {
1698 let mut r = ModeRegistry::new();
1699 let one = r.register(MockMode::minor("a-mode")).unwrap();
1700 let two = r.register(MockMode::minor("b-mode")).unwrap();
1701 let three = r.register(MockMode::minor("c-mode")).unwrap();
1702 let mut a = ActiveModes::new();
1703 let g = GuardStoreHandle::new();
1704 let bus = evts();
1705 let mut rx = subscribe_lifecycle(&bus);
1706 for id in [one, two, three] {
1707 r.activate_minor(
1708 &mut a,
1709 &g,
1710 &cfg(),
1711 &bus,
1712 &svcs(),
1713 buf(),
1714 id,
1715 CapabilitySet::empty(),
1716 )
1717 .unwrap();
1718 let _activated = await_lifecycle(&mut rx).await;
1719 }
1720 assert_eq!(a.minors(), &[one, two, three]);
1721 }
1722
1723 #[tokio::test]
1724 async fn minor_re_activation_is_noop() {
1725 let mock = MockMode::minor("a-mode");
1726 let act = mock.activate_calls.clone();
1727 let mut r = ModeRegistry::new();
1728 let id = r.register(mock).unwrap();
1729 let mut a = ActiveModes::new();
1730 let g = GuardStoreHandle::new();
1731 let bus = evts();
1732 let mut rx = subscribe_lifecycle(&bus);
1733 r.activate_minor(
1734 &mut a,
1735 &g,
1736 &cfg(),
1737 &bus,
1738 &svcs(),
1739 buf(),
1740 id,
1741 CapabilitySet::empty(),
1742 )
1743 .unwrap();
1744 let _first = await_lifecycle(&mut rx).await;
1745 r.activate_minor(
1747 &mut a,
1748 &g,
1749 &cfg(),
1750 &bus,
1751 &svcs(),
1752 buf(),
1753 id,
1754 CapabilitySet::empty(),
1755 )
1756 .unwrap();
1757 tokio::task::yield_now().await;
1759 assert!(
1760 rx.try_recv().is_err(),
1761 "double-activation should not publish a second event"
1762 );
1763 assert_eq!(act.load(Ordering::SeqCst), 1);
1764 }
1765
1766 #[tokio::test]
1767 async fn implies_auto_activates_dependency() {
1768 let mut r = ModeRegistry::new();
1769 let lnum = r.register(MockMode::minor("line-numbers-mode")).unwrap();
1770 let rlnum = r
1771 .register(MockMode::minor("relative-line-numbers-mode").implying(lnum))
1772 .unwrap();
1773 let mut a = ActiveModes::new();
1774 let g = GuardStoreHandle::new();
1775 let bus = evts();
1776 let mut rx = subscribe_lifecycle(&bus);
1777 r.activate_minor(
1778 &mut a,
1779 &g,
1780 &cfg(),
1781 &bus,
1782 &svcs(),
1783 buf(),
1784 rlnum,
1785 CapabilitySet::empty(),
1786 )
1787 .unwrap();
1788 let evt = await_lifecycle(&mut rx).await;
1793 assert!(
1794 matches!(evt, Event::MinorActivated { ref minor, .. } if minor.as_str() == rlnum.as_str()),
1795 "parent (rlnum) should activate first; got {evt:?}",
1796 );
1797 let evt = await_lifecycle(&mut rx).await;
1798 assert!(
1799 matches!(evt, Event::MinorActivated { ref minor, .. } if minor.as_str() == lnum.as_str()),
1800 "implied child (lnum) should activate second; got {evt:?}",
1801 );
1802 assert!(a.has_minor(rlnum));
1803 assert!(a.has_minor(lnum));
1804 }
1805
1806 #[tokio::test]
1814 async fn declaring_a_refresh_action_auto_activates_the_shared_minor() {
1815 let mut r = ModeRegistry::new();
1816 crate::refreshable_view_mode::register_refreshable_view_mode(&mut r);
1817 let view = r
1818 .register(MockMode::minor("some-view-mode").refreshing("action:some-view-refresh"))
1819 .unwrap();
1820 let mut a = ActiveModes::new();
1821 let g = GuardStoreHandle::new();
1822 r.activate_minor(
1823 &mut a,
1824 &g,
1825 &cfg(),
1826 &evts(),
1827 &svcs(),
1828 buf(),
1829 view,
1830 CapabilitySet::empty(),
1831 )
1832 .unwrap();
1833 assert!(
1834 a.has_minor(crate::RefreshableViewMode::mode_id()),
1835 "a mode declaring refresh_action must get the shared `gr` minor \
1836 without also listing it in implies()"
1837 );
1838 }
1839
1840 #[tokio::test]
1841 async fn a_major_declaring_a_refresh_action_also_gets_the_shared_minor() {
1842 let mut r = ModeRegistry::new();
1843 crate::refreshable_view_mode::register_refreshable_view_mode(&mut r);
1844 let major = r
1845 .register(MockMode::major("some-view-mode").refreshing("action:some-view-refresh"))
1846 .unwrap();
1847 let mut a = ActiveModes::new();
1848 let g = GuardStoreHandle::new();
1849 r.activate_major(
1850 &mut a,
1851 &g,
1852 &cfg(),
1853 &evts(),
1854 &svcs(),
1855 buf(),
1856 major,
1857 CapabilitySet::empty(),
1858 )
1859 .unwrap();
1860 assert!(
1861 a.has_minor(crate::RefreshableViewMode::mode_id()),
1862 "the cascade must fire for majors too — magit / compilation \
1863 declare their refresh on the major"
1864 );
1865 }
1866
1867 #[tokio::test]
1868 async fn no_refresh_action_leaves_the_shared_minor_off() {
1869 let mut r = ModeRegistry::new();
1870 crate::refreshable_view_mode::register_refreshable_view_mode(&mut r);
1871 let plain = r.register(MockMode::minor("plain-mode")).unwrap();
1872 let mut a = ActiveModes::new();
1873 let g = GuardStoreHandle::new();
1874 r.activate_minor(
1875 &mut a,
1876 &g,
1877 &cfg(),
1878 &evts(),
1879 &svcs(),
1880 buf(),
1881 plain,
1882 CapabilitySet::empty(),
1883 )
1884 .unwrap();
1885 assert!(
1886 !a.has_minor(crate::RefreshableViewMode::mode_id()),
1887 "`gr` must not attach to ordinary buffers — it is LSP references there"
1888 );
1889 }
1890
1891 #[tokio::test]
1895 async fn missing_shared_minor_does_not_fail_activation() {
1896 let mut r = ModeRegistry::new();
1897 let view = r
1898 .register(MockMode::minor("some-view-mode").refreshing("action:some-view-refresh"))
1899 .unwrap();
1900 let mut a = ActiveModes::new();
1901 let g = GuardStoreHandle::new();
1902 let res = r.activate_minor(
1903 &mut a,
1904 &g,
1905 &cfg(),
1906 &evts(),
1907 &svcs(),
1908 buf(),
1909 view,
1910 CapabilitySet::empty(),
1911 );
1912 assert!(res.is_ok(), "missing shared minor must degrade, not fail");
1913 assert!(a.has_minor(view));
1914 }
1915
1916 #[tokio::test]
1917 async fn implies_unregistered_dependency_fails() {
1918 let mut r = ModeRegistry::new();
1919 let phantom = ModeId::new("ghost-mode");
1920 let id = r
1921 .register(MockMode::minor("thing-mode").implying(phantom))
1922 .unwrap();
1923 let mut a = ActiveModes::new();
1924 let g = GuardStoreHandle::new();
1925 let err = r
1926 .activate_minor(
1927 &mut a,
1928 &g,
1929 &cfg(),
1930 &evts(),
1931 &svcs(),
1932 buf(),
1933 id,
1934 CapabilitySet::empty(),
1935 )
1936 .unwrap_err();
1937 assert!(matches!(
1938 err,
1939 ModeActivationError::UnregisteredDependency { .. }
1940 ));
1941 }
1942
1943 #[tokio::test]
1944 async fn conflict_blocks_activation() {
1945 let mut r = ModeRegistry::new();
1946 let one_id = ModeId::new("vim-paste-mode");
1947 let two_id = ModeId::new("auto-pair-mode");
1948 let one = r
1949 .register(MockMode::minor("vim-paste-mode").conflicting_with(two_id))
1950 .unwrap();
1951 let two = r.register(MockMode::minor("auto-pair-mode")).unwrap();
1952 assert_eq!(one, one_id);
1953 assert_eq!(two, two_id);
1954 let mut a = ActiveModes::new();
1955 let g = GuardStoreHandle::new();
1956 let bus = evts();
1957 let mut rx = subscribe_lifecycle(&bus);
1958 r.activate_minor(
1959 &mut a,
1960 &g,
1961 &cfg(),
1962 &bus,
1963 &svcs(),
1964 buf(),
1965 two,
1966 CapabilitySet::empty(),
1967 )
1968 .unwrap();
1969 let _two_activated = await_lifecycle(&mut rx).await;
1970 let err = r
1971 .activate_minor(
1972 &mut a,
1973 &g,
1974 &cfg(),
1975 &bus,
1976 &svcs(),
1977 buf(),
1978 one,
1979 CapabilitySet::empty(),
1980 )
1981 .unwrap_err();
1982 match err {
1983 ModeActivationError::Conflict { mode, active } => {
1984 assert_eq!(mode, one);
1985 assert_eq!(active, two);
1986 }
1987 _ => panic!("expected Conflict"),
1988 }
1989 }
1990
1991 #[tokio::test]
1992 async fn deactivate_minor_drops_guard_and_publishes_event() {
1993 let mock = MockMode::minor("a-mode");
1994 let deact = mock.deactivate_calls.clone();
1995 let mut r = ModeRegistry::new();
1996 let id = r.register(mock).unwrap();
1997 let mut a = ActiveModes::new();
1998 let g = GuardStoreHandle::new();
1999 let bus = evts();
2000 let mut rx = subscribe_lifecycle(&bus);
2001 r.activate_minor(
2002 &mut a,
2003 &g,
2004 &cfg(),
2005 &bus,
2006 &svcs(),
2007 buf(),
2008 id,
2009 CapabilitySet::empty(),
2010 )
2011 .unwrap();
2012 let _activated = await_lifecycle(&mut rx).await;
2013 r.deactivate_minor(&mut a, &g, &bus, buf(), id).unwrap();
2014 let evt = await_lifecycle(&mut rx).await;
2015 assert!(matches!(evt, Event::MinorDeactivated { minor, .. } if minor == id.as_str()));
2016 assert_eq!(deact.load(Ordering::SeqCst), 1, "Guard::Drop ran");
2017 assert!(!a.has_minor(id));
2018 assert!(!g.contains(buf(), id));
2019 }
2020
2021 #[tokio::test]
2022 async fn deactivate_inactive_minor_is_noop() {
2023 let mut r = ModeRegistry::new();
2024 let id = r.register(MockMode::minor("a-mode")).unwrap();
2025 let mut a = ActiveModes::new();
2026 let g = GuardStoreHandle::new();
2027 let bus = evts();
2028 let mut rx = subscribe_lifecycle(&bus);
2029 r.deactivate_minor(&mut a, &g, &bus, buf(), id).unwrap();
2030 tokio::task::yield_now().await;
2031 assert!(
2032 rx.try_recv().is_err(),
2033 "no-op deactivate should not publish"
2034 );
2035 }
2036
2037 #[tokio::test]
2038 async fn deactivate_major_drops_guard_and_clears() {
2039 let mock = MockMode::major("rust-mode");
2040 let deact = mock.deactivate_calls.clone();
2041 let mut r = ModeRegistry::new();
2042 let id = r.register(mock).unwrap();
2043 let mut a = ActiveModes::new();
2044 let g = GuardStoreHandle::new();
2045 let bus = evts();
2046 let mut rx = subscribe_lifecycle(&bus);
2047 r.activate_major(
2048 &mut a,
2049 &g,
2050 &cfg(),
2051 &bus,
2052 &svcs(),
2053 buf(),
2054 id,
2055 CapabilitySet::empty(),
2056 )
2057 .unwrap();
2058 let _entered = await_lifecycle(&mut rx).await;
2059 r.deactivate_major(&mut a, &g, &bus, buf()).unwrap();
2060 let evt = await_lifecycle(&mut rx).await;
2061 assert!(matches!(evt, Event::MajorExiting { major, .. } if major == id.as_str()));
2062 assert_eq!(deact.load(Ordering::SeqCst), 1);
2063 assert_eq!(a.major(), None);
2064 assert!(!g.contains(buf(), id));
2065 }
2066
2067 #[tokio::test]
2068 async fn deactivate_major_when_none_active_is_noop() {
2069 let r = ModeRegistry::new();
2070 let mut a = ActiveModes::new();
2071 let g = GuardStoreHandle::new();
2072 let bus = evts();
2073 let mut rx = subscribe_lifecycle(&bus);
2074 r.deactivate_major(&mut a, &g, &bus, buf()).unwrap();
2075 tokio::task::yield_now().await;
2076 assert!(rx.try_recv().is_err());
2077 }
2078
2079 #[tokio::test]
2084 async fn lifecycle_err_publishes_activation_failed() {
2085 struct FailingMode {
2086 id: ModeId,
2087 }
2088 impl Mode for FailingMode {
2089 type Guard = ();
2090 fn id(&self) -> ModeId {
2091 self.id
2092 }
2093 fn kind(&self) -> ModeKind {
2094 ModeKind::Minor
2095 }
2096 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
2097 let id = self.id;
2098 Box::pin(async move { Err(ModeActivationError::NotRegistered(id)) })
2099 }
2100 }
2101 let mut r = ModeRegistry::new();
2102 let id = r
2103 .register(FailingMode {
2104 id: ModeId::new("fail-mode"),
2105 })
2106 .unwrap();
2107 let mut a = ActiveModes::new();
2108 let g = GuardStoreHandle::new();
2109 let bus = evts();
2110 let mut rx = subscribe_mode_events(&bus);
2111 r.activate_minor(
2112 &mut a,
2113 &g,
2114 &cfg(),
2115 &bus,
2116 &svcs(),
2117 buf(),
2118 id,
2119 CapabilitySet::empty(),
2120 )
2121 .unwrap();
2122 let evt = await_event(&mut rx).await;
2123 match evt {
2124 ModeEvent::ModeActivationFailed { mode, reason, .. } => {
2125 assert_eq!(mode, id);
2126 assert!(reason.contains("not registered"));
2127 }
2128 other => panic!("expected ModeActivationFailed, got {other:?}"),
2129 }
2130 assert!(
2131 !g.contains(buf(), id),
2132 "no Guard stashed on lifecycle error"
2133 );
2134 }
2135
2136 #[tokio::test]
2144 async fn cascade_abort_publishes_synthetic_failures_for_unrun_steps() {
2145 struct FailingParent {
2146 id: ModeId,
2147 implies: Vec<ModeId>,
2148 }
2149 impl Mode for FailingParent {
2150 type Guard = ();
2151 fn id(&self) -> ModeId {
2152 self.id
2153 }
2154 fn kind(&self) -> ModeKind {
2155 ModeKind::Minor
2156 }
2157 fn implies(&self) -> &[ModeId] {
2158 &self.implies
2159 }
2160 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
2161 let id = self.id;
2162 Box::pin(async move { Err(ModeActivationError::NotRegistered(id)) })
2163 }
2164 }
2165 let mut r = ModeRegistry::new();
2166 let child_id = r.register(MockMode::minor("child-mode")).unwrap();
2167 let parent_id = ModeId::new("parent-mode");
2168 r.register(FailingParent {
2169 id: parent_id,
2170 implies: vec![child_id],
2171 })
2172 .unwrap();
2173 let mut a = ActiveModes::new();
2174 let g = GuardStoreHandle::new();
2175 let bus = evts();
2176 let mut rx = subscribe_mode_events(&bus);
2177 r.activate_minor(
2178 &mut a,
2179 &g,
2180 &cfg(),
2181 &bus,
2182 &svcs(),
2183 buf(),
2184 parent_id,
2185 CapabilitySet::empty(),
2186 )
2187 .unwrap();
2188 assert!(a.has_minor(parent_id));
2190 assert!(a.has_minor(child_id));
2191 let evt = await_event(&mut rx).await;
2193 match evt {
2194 ModeEvent::ModeActivationFailed { mode, reason, .. } => {
2195 assert_eq!(mode, parent_id);
2196 assert!(
2197 !reason.starts_with("cascade aborted"),
2198 "parent's reason should be the real error, not the synthetic prefix; got {reason:?}",
2199 );
2200 }
2201 other => panic!("expected parent's ModeActivationFailed first, got {other:?}"),
2202 }
2203 let evt = await_event(&mut rx).await;
2205 match evt {
2206 ModeEvent::ModeActivationFailed { mode, reason, .. } => {
2207 assert_eq!(mode, child_id);
2208 assert!(
2209 reason.contains("cascade aborted"),
2210 "child's reason should announce cascade abort; got {reason:?}",
2211 );
2212 assert!(reason.contains(parent_id.as_str()));
2213 }
2214 other => panic!("expected child's synthetic ModeActivationFailed, got {other:?}"),
2215 }
2216 assert!(!g.contains(buf(), parent_id));
2219 assert!(!g.contains(buf(), child_id));
2220 }
2221
2222 #[tokio::test]
2237 async fn rapid_deactivate_during_pending_activate_drops_guard_on_spawn_side() {
2238 use tokio::sync::oneshot;
2239
2240 struct DropTrackingGuard {
2244 counter: StdArc<AtomicU32>,
2245 }
2246 impl Drop for DropTrackingGuard {
2247 fn drop(&mut self) {
2248 self.counter.fetch_add(1, Ordering::SeqCst);
2249 }
2250 }
2251
2252 struct GatedMode {
2257 id: ModeId,
2258 gate_rx: std::sync::Mutex<Option<oneshot::Receiver<()>>>,
2259 drop_counter: StdArc<AtomicU32>,
2260 }
2261 impl Mode for GatedMode {
2262 type Guard = DropTrackingGuard;
2263 fn id(&self) -> ModeId {
2264 self.id
2265 }
2266 fn kind(&self) -> ModeKind {
2267 ModeKind::Minor
2268 }
2269 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
2270 let rx = self
2271 .gate_rx
2272 .lock()
2273 .unwrap()
2274 .take()
2275 .expect("oneshot consumed twice");
2276 let counter = self.drop_counter.clone();
2277 Box::pin(async move {
2278 let _ = rx.await;
2280 Ok(DropTrackingGuard { counter })
2281 })
2282 }
2283 }
2284
2285 let drop_counter = StdArc::new(AtomicU32::new(0));
2286 let (gate_tx, gate_rx) = oneshot::channel::<()>();
2287 let mode = GatedMode {
2288 id: ModeId::new("gated-mode"),
2289 gate_rx: std::sync::Mutex::new(Some(gate_rx)),
2290 drop_counter: drop_counter.clone(),
2291 };
2292 let mut r = ModeRegistry::new();
2293 let id = r.register(mode).unwrap();
2294 let mut a = ActiveModes::new();
2295 let g = GuardStoreHandle::new();
2296 let bus = evts();
2297 let mut rx = subscribe_lifecycle(&bus);
2298
2299 r.activate_minor(
2303 &mut a,
2304 &g,
2305 &cfg(),
2306 &bus,
2307 &svcs(),
2308 buf(),
2309 id,
2310 CapabilitySet::empty(),
2311 )
2312 .unwrap();
2313 assert!(a.has_minor(id));
2314 assert!(
2315 !g.contains(buf(), id),
2316 "Guard not stashed yet (spawn pending)"
2317 );
2318
2319 r.deactivate_minor(&mut a, &g, &bus, buf(), id).unwrap();
2325 assert!(!a.has_minor(id));
2326 let evt = await_lifecycle(&mut rx).await;
2328 assert!(
2329 matches!(evt, Event::MinorDeactivated { minor, .. } if minor == id.as_str()),
2330 "deactivate should publish MinorDeactivated synchronously",
2331 );
2332 assert_eq!(
2333 drop_counter.load(Ordering::SeqCst),
2334 0,
2335 "Guard hasn't been constructed yet -- spawn is parked on oneshot",
2336 );
2337
2338 gate_tx.send(()).unwrap();
2344 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
2350 while drop_counter.load(Ordering::SeqCst) == 0 {
2351 if std::time::Instant::now() >= deadline {
2352 break;
2353 }
2354 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
2355 }
2356 assert_eq!(
2357 drop_counter.load(Ordering::SeqCst),
2358 1,
2359 "stale Guard's Drop should have fired on the spawn side",
2360 );
2361 assert!(
2362 !g.contains(buf(), id),
2363 "Guard must NOT be stashed in the store -- the deactivate already happened",
2364 );
2365 assert!(
2368 rx.try_recv().is_err(),
2369 "stale activation should not publish MinorActivated",
2370 );
2371 }
2372}