Skip to main content

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}