lattice_mode/active.rs
1//! `ActiveModes`: the major + ordered minors set per buffer.
2//!
3//! M.3 lands `ActiveModes` as a field on `Document`; M.1 keeps
4//! it as a standalone type that tests can construct directly.
5//! The registry's activation / deactivation methods take
6//! `&mut ActiveModes` as a parameter so they can mutate the set
7//! without owning per-buffer state -- letting `lattice-mode` be
8//! buffer-storage-agnostic for the foundation slice.
9//!
10//! Minor mode order matters (it's the activation order; later
11//! activations override earlier ones at the option-resolution
12//! layer per `mode-architecture.md` §6.2). The registry keeps
13//! this ordering stable: activate appends to the end,
14//! deactivate removes by id without disturbing relative order
15//! of the survivors.
16
17use smallvec::SmallVec;
18
19use crate::mode::ModeId;
20
21/// The set of modes active on a buffer. Exactly one major (or
22/// none, prior to first major activation), plus any number of
23/// minors in activation order.
24///
25/// Ordering invariant: minors are stored in the order they were
26/// activated; when an active mode is deactivated, the survivors
27/// retain their relative positions (no reshuffling). This is
28/// what M.2's option-resolution layer relies on for "later
29/// activation wins" tie-breaking.
30///
31/// `SmallVec` keeps the typical case (0-4 minors) inline. A
32/// buffer with more active minors spills to the heap; that's
33/// fine -- the hot path is option resolution against the cached
34/// `ResolvedOptions`, not iteration of `ActiveModes`.
35#[derive(Debug, Default, Clone)]
36pub struct ActiveModes {
37 major: Option<ModeId>,
38 minors: SmallVec<[ModeId; 4]>,
39}
40
41impl ActiveModes {
42 /// Construct an empty set (no major, no minors).
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 /// The active major, if any. `None` until the first major
48 /// activation runs (e.g. a freshly-opened scratch buffer
49 /// before mode resolution).
50 pub fn major(&self) -> Option<ModeId> {
51 self.major
52 }
53
54 /// All active minors, in activation order.
55 pub fn minors(&self) -> &[ModeId] {
56 &self.minors
57 }
58
59 /// The mode ids the keymap lookup must gate on, in priority
60 /// order: the active major first (lowest of the gated layers),
61 /// then the minors in activation order (each overlaying the
62 /// previous). Pass this to
63 /// `KeymapHandle::lookup_with_context` so a `MajorMode` layer
64 /// fires only in buffers where that major is active — a bare
65 /// `minors()` slice would leave major-mode chords ungated
66 /// (they'd fire everywhere).
67 pub fn keymap_gated_ids(&self) -> Vec<ModeId> {
68 let mut ids = Vec::with_capacity(self.minors.len() + 1);
69 ids.extend(self.major);
70 ids.extend(self.minors.iter().copied());
71 ids
72 }
73
74 /// True iff this minor is currently active.
75 pub fn has_minor(&self, mode: ModeId) -> bool {
76 self.minors.contains(&mode)
77 }
78
79 /// True iff `mode` is the active major.
80 pub fn is_active_major(&self, mode: ModeId) -> bool {
81 self.major == Some(mode)
82 }
83
84 /// True iff `mode` is active in any role (major or minor).
85 pub fn is_active(&self, mode: ModeId) -> bool {
86 self.is_active_major(mode) || self.has_minor(mode)
87 }
88
89 // -------- Mutation API: registry-only --------
90 //
91 // These methods are pub(crate) so only the `registry` module
92 // can drive transitions. External callers go through
93 // `ModeRegistry::activate_*` / `deactivate_*` which validate
94 // capabilities + conflicts before calling these.
95
96 /// Set the major mode. Registry calls this AFTER any
97 /// previous major's `on_deactivate` ran and AFTER the new
98 /// major's `on_activate` ran successfully.
99 pub(crate) fn set_major(&mut self, mode: Option<ModeId>) {
100 self.major = mode;
101 }
102
103 /// Append a minor in activation order. Registry calls this
104 /// AFTER the minor's `on_activate` ran successfully.
105 ///
106 /// **Move-to-end on re-activation** (K.1.a, 2026-05-30):
107 /// if the minor was already active, it's moved to the end
108 /// of the list rather than left in place. This makes
109 /// re-activation re-assert the mode as most-recent, which
110 /// is the source of truth for both option-resolution
111 /// tie-breaking (per `mode-architecture.md` §6.2) and the
112 /// per-buffer keymap layer ordering (K.1.c). Matches
113 /// emacs's `minor-mode-map-alist` re-promotion semantics.
114 pub(crate) fn push_minor(&mut self, mode: ModeId) {
115 if let Some(idx) = self.minors.iter().position(|&m| m == mode) {
116 self.minors.remove(idx);
117 }
118 self.minors.push(mode);
119 }
120
121 /// Remove a minor by id. Registry calls this AFTER the
122 /// minor's `on_deactivate` ran. Returns true iff the minor
123 /// was actually active (no-op if not).
124 pub(crate) fn remove_minor(&mut self, mode: ModeId) -> bool {
125 if let Some(idx) = self.minors.iter().position(|&m| m == mode) {
126 self.minors.remove(idx);
127 true
128 } else {
129 false
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 #![allow(clippy::unwrap_used)]
137 use super::*;
138
139 #[test]
140 fn defaults_are_empty() {
141 let a = ActiveModes::new();
142 assert_eq!(a.major(), None);
143 assert!(a.minors().is_empty());
144 }
145
146 #[test]
147 fn set_major_round_trips() {
148 let mut a = ActiveModes::new();
149 let m = ModeId::new("rust-mode");
150 a.set_major(Some(m));
151 assert_eq!(a.major(), Some(m));
152 assert!(a.is_active_major(m));
153 assert!(a.is_active(m));
154 }
155
156 #[test]
157 fn minors_preserve_activation_order() {
158 let mut a = ActiveModes::new();
159 let one = ModeId::new("lsp-mode");
160 let two = ModeId::new("lsp-completion-mode");
161 let three = ModeId::new("lsp-diagnostics-mode");
162 a.push_minor(one);
163 a.push_minor(two);
164 a.push_minor(three);
165 assert_eq!(a.minors(), &[one, two, three]);
166 }
167
168 #[test]
169 fn push_minor_is_idempotent() {
170 let mut a = ActiveModes::new();
171 let m = ModeId::new("git-blame-mode");
172 a.push_minor(m);
173 a.push_minor(m);
174 assert_eq!(a.minors(), &[m]);
175 }
176
177 /// K.1.a (2026-05-30): re-activating a minor that's
178 /// already active moves it to the end (most-recent),
179 /// matching emacs's `minor-mode-map-alist` re-promotion.
180 /// This is what makes "later activation wins" semantics
181 /// hold under re-activation, not just first-activation.
182 #[test]
183 fn push_minor_moves_to_end_on_reactivation() {
184 let mut a = ActiveModes::new();
185 let one = ModeId::new("a-mode");
186 let two = ModeId::new("b-mode");
187 let three = ModeId::new("c-mode");
188 a.push_minor(one);
189 a.push_minor(two);
190 a.push_minor(three);
191 // Re-activate `one` — should move from index 0 to
192 // the end without otherwise disturbing the order.
193 a.push_minor(one);
194 assert_eq!(a.minors(), &[two, three, one]);
195 }
196
197 #[test]
198 fn remove_minor_preserves_relative_order() {
199 let mut a = ActiveModes::new();
200 let one = ModeId::new("a-mode");
201 let two = ModeId::new("b-mode");
202 let three = ModeId::new("c-mode");
203 a.push_minor(one);
204 a.push_minor(two);
205 a.push_minor(three);
206 assert!(a.remove_minor(two));
207 assert_eq!(a.minors(), &[one, three]);
208 }
209
210 #[test]
211 fn remove_minor_returns_false_for_inactive() {
212 let mut a = ActiveModes::new();
213 assert!(!a.remove_minor(ModeId::new("never-activated-mode")));
214 }
215}