Skip to main content

lattice_config/
resolver.rs

1//! `Resolver`: walks layered overrides and produces a
2//! [`crate::ResolvedOptions`] cache for one buffer
3//! (`mode-architecture.md` §6.1).
4//!
5//! Layer priority (highest to lowest):
6//! 1. Modal-state override
7//! 2. Buffer-local explicit set (`:setlocal`)
8//! 3. Active minor modes (in activation order; `OverridePriority`
9//!    breaks ties)
10//! 4. Major mode
11//! 5. Global (the registry's current value)
12//! 6. Built-in default (the option's `default_value()`)
13//!
14//! For scalars: first non-empty layer wins. For collections
15//! (statusline contributors, decoration providers, completion
16//! sources) the layers concatenate; that's a layer-aware policy
17//! that the resolver applies based on the option's value type.
18//! M.2.0a's resolver is the scalar-only path; collection-shaped
19//! options land in M.2.1 alongside the actual mode integrations
20//! that produce them.
21//!
22//! ## Default-value resolution
23//!
24//! M.2.0a's resolver doesn't itself supply layer 6 (built-in
25//! defaults). The expectation is that the registry pre-populates
26//! the resolved cache with defaults via a one-time bootstrap,
27//! and the resolver's per-recompute walk overlays the higher-
28//! priority layers on top. This keeps the per-recompute cost
29//! bounded to "options that have at least one override" rather
30//! than re-iterating every registered option on every layer
31//! change. Bootstrap is M.2.0b's territory (when migration of
32//! built-in options to the macro path lets the registry
33//! enumerate them via the linkme slice). Until then, callers
34//! prepopulate with default values explicitly; tests do this
35//! directly.
36
37use std::any::TypeId;
38
39use crate::origin::OptionOrigin;
40use crate::overrides::{OptionOverride, OptionOverrideSet, OverridePriority};
41use crate::resolved::ResolvedOptions;
42
43/// Walks layered overrides and emits a fresh [`ResolvedOptions`].
44///
45/// The resolver is stateless -- it's just an algorithm. Callers
46/// typically own the cache and ask the resolver to refill it
47/// via [`Self::resolve_into`].
48#[derive(Default)]
49pub struct Resolver;
50
51impl Resolver {
52    /// The resolver. Zero-sized; equivalent to `Resolver::default()`.
53    pub fn new() -> Self {
54        Self
55    }
56
57    /// Walk `layers` (highest priority first) and write resolved
58    /// values into `out`. Each layer is an iterable of
59    /// [`OptionOverride`]s in the layer's own internal order.
60    /// Within a layer, last-pushed wins for the same option
61    /// type; across layers, higher priority wins.
62    ///
63    /// Existing entries in `out` are preserved unless overridden
64    /// by a layer; this lets callers seed `out` with defaults
65    /// (via the registry's default-bootstrap, M.2.0b) and have
66    /// the resolver overlay only what changed.
67    ///
68    /// `OverridePriority::High` wins regardless of layer
69    /// position; `Low` only wins when no `Normal`/`High` covers
70    /// the option. The one thing `High` does NOT beat is a user's
71    /// own per-buffer value — but that needs origins, so it applies
72    /// only through [`Self::resolve_into_with_origins`] with a layer
73    /// tagged [`OptionOrigin::BufferLocal`]. Within a single layer, two
74    /// overrides at the same priority resolve to last-pushed (per
75    /// `mode-architecture.md` §6.2 conflict policy; M.2.1 hooks
76    /// this to a `ModeEvent::OptionConflict` emission).
77    ///
78    /// Origin is not tracked (every winner is recorded as
79    /// [`OptionOrigin::GlobalConfig`]); use
80    /// [`Self::resolve_into_with_origins`] when `:set name?` /
81    /// `:setlocal name?` echo is needed.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use std::any::TypeId;
87    /// use lattice_config::{
88    ///     OptionOverride, OptionOverrideSet, OverridePriority, ResolvedOptions, Resolver,
89    ///     Tabstop, Wrap,
90    /// };
91    ///
92    /// // Seed with the "global" values, as the registry bootstrap would.
93    /// let mut out = ResolvedOptions::new();
94    /// out.insert::<Tabstop>(8);
95    /// out.insert::<Wrap>(true);
96    ///
97    /// // Highest-priority layer first: a minor mode, then a major mode.
98    /// let minor: OptionOverrideSet =
99    ///     [OptionOverride::new(TypeId::of::<Tabstop>(), 2_i64)].into_iter().collect();
100    /// let major: OptionOverrideSet = [
101    ///     OptionOverride::new(TypeId::of::<Tabstop>(), 4_i64),
102    ///     OptionOverride::with_priority(TypeId::of::<Wrap>(), false, OverridePriority::High),
103    /// ]
104    /// .into_iter()
105    /// .collect();
106    ///
107    /// Resolver::new().resolve_into([&minor, &major], &mut out);
108    /// assert_eq!(*out.get::<Tabstop>().unwrap(), 2); // higher layer wins among Normals
109    /// assert_eq!(*out.get::<Wrap>().unwrap(), false); // High wins from a lower layer
110    /// ```
111    pub fn resolve_into<'a, L>(&self, layers: L, out: &mut ResolvedOptions)
112    where
113        L: IntoIterator<Item = &'a OptionOverrideSet>,
114    {
115        // Delegate to the origin-aware path, tagging every layer
116        // with `GlobalConfig` as a neutral fallback. The bootstrap
117        // already wrote the correct origin before this is called.
118        self.resolve_into_with_origins(
119            layers
120                .into_iter()
121                .map(|set| (set, OptionOrigin::GlobalConfig)),
122            out,
123        );
124    }
125
126    /// Origin-aware resolution. Each element is an
127    /// `(&OptionOverrideSet, OptionOrigin)` pair; the origin is
128    /// recorded alongside the winning value in `out`. The caller is
129    /// responsible for assigning the correct [`OptionOrigin`] to each
130    /// layer (e.g. `BufferLocal` for the buffer-local override set,
131    /// `ModeContribution { mode_id }` for each mode's set).
132    ///
133    /// # Examples
134    ///
135    /// A `:setlocal` value beats a mode's `High` contribution:
136    ///
137    /// ```
138    /// use std::any::TypeId;
139    /// use lattice_config::{
140    ///     OptionOrigin, OptionOverride, OptionOverrideSet, OverridePriority, ResolvedOptions,
141    ///     Resolver, Tabstop,
142    /// };
143    ///
144    /// let local: OptionOverrideSet =
145    ///     [OptionOverride::new(TypeId::of::<Tabstop>(), 3_i64)].into_iter().collect();
146    /// let mode: OptionOverrideSet = [OptionOverride::with_priority(
147    ///     TypeId::of::<Tabstop>(),
148    ///     8_i64,
149    ///     OverridePriority::High,
150    /// )]
151    /// .into_iter()
152    /// .collect();
153    ///
154    /// let mut out = ResolvedOptions::new();
155    /// Resolver::new().resolve_into_with_origins(
156    ///     [
157    ///         (&local, OptionOrigin::BufferLocal),
158    ///         (&mode, OptionOrigin::ModeContribution { mode_id: "rust-mode".into() }),
159    ///     ],
160    ///     &mut out,
161    /// );
162    /// assert_eq!(*out.get::<Tabstop>().unwrap(), 3);
163    /// assert_eq!(out.get_origin::<Tabstop>(), OptionOrigin::BufferLocal);
164    /// assert_eq!(out.get_origin::<Tabstop>().to_string(), "buffer-local");
165    /// ```
166    pub fn resolve_into_with_origins<'a>(
167        &self,
168        layers: impl IntoIterator<Item = (&'a OptionOverrideSet, OptionOrigin)>,
169        out: &mut ResolvedOptions,
170    ) {
171        let mut winners: std::collections::HashMap<TypeId, Candidate<'_>> =
172            std::collections::HashMap::new();
173
174        for (layer_idx, (set, origin)) in layers.into_iter().enumerate() {
175            let layer_rank = usize::MAX - layer_idx;
176            for (pos, ov) in set.iter().enumerate() {
177                let candidate = Candidate {
178                    ov,
179                    layer_rank,
180                    within_layer_pos: pos,
181                    origin: origin.clone(),
182                };
183                match winners.get(&ov.option_type_id) {
184                    None => {
185                        winners.insert(ov.option_type_id, candidate);
186                    }
187                    Some(existing) => {
188                        if Self::candidate_better(&candidate, existing) {
189                            winners.insert(ov.option_type_id, candidate);
190                        }
191                    }
192                }
193            }
194        }
195
196        for (type_id, c) in winners {
197            out.insert_erased_with_origin(type_id, c.ov.value.clone(), c.origin);
198        }
199    }
200
201    /// "Is `a` more authoritative than `b`?" Used during the
202    /// merge walk. Order: a user's per-buffer set outranks any mode
203    /// contribution; then `OverridePriority::High` wins and `Low` loses;
204    /// among `Normal`s, higher layer rank wins; within a layer, later
205    /// position wins.
206    fn candidate_better(a: &Candidate<'_>, b: &Candidate<'_>) -> bool {
207        // **A user's explicit per-buffer value beats any mode's, priority
208        // included.** Checked BEFORE priority, which is the whole point: the
209        // rule below makes `High` win absolute, so without this a mode
210        // declaring `High` was unoverridable from a user's config — and any
211        // mode may declare it.
212        //
213        // This is the behaviour the mode-option seam already claimed. A plugin
214        // declaring `foldmethod` for its buffers documents it as "a LAYER, not
215        // a write … a `:setlocal` in that buffer still wins over it, which is
216        // the right way round — the user gets the last word in their own
217        // buffer." That was true only against `Normal` contributions.
218        //
219        // `BufferLocal` only, NOT `GlobalConfig`. Global config is the
220        // baseline a mode is *supposed* to refine — org setting
221        // `foldmethod=syntax` over a global `foldmethod=indent` is the seam
222        // working, not a conflict. A buffer-local set is a different act: it
223        // names one buffer, so there is no reading of it under which the mode
224        // is the more specific answer.
225        //
226        // Mode-versus-mode is untouched, so `read-only-mode`'s `High` on
227        // `writable=false` still beats every other mode regardless of
228        // activation order — which is the threat model that rule was written
229        // for. What changes is only that the person who owns the editor can
230        // now say otherwise about one buffer.
231        let outranks_by_authorship = |x: &Candidate<'_>, y: &Candidate<'_>| {
232            matches!(x.origin, OptionOrigin::BufferLocal)
233                && matches!(y.origin, OptionOrigin::ModeContribution { .. })
234        };
235        if outranks_by_authorship(a, b) {
236            return true;
237        }
238        if outranks_by_authorship(b, a) {
239            return false;
240        }
241        // Explicit-priority wins absolute.
242        if a.ov.priority == OverridePriority::High && b.ov.priority != OverridePriority::High {
243            return true;
244        }
245        if b.ov.priority == OverridePriority::High {
246            return false;
247        }
248        if a.ov.priority == OverridePriority::Low && b.ov.priority != OverridePriority::Low {
249            return false;
250        }
251        if b.ov.priority == OverridePriority::Low {
252            return true;
253        }
254        // Normal vs Normal: layer first, then position within layer.
255        if a.layer_rank != b.layer_rank {
256            return a.layer_rank > b.layer_rank;
257        }
258        a.within_layer_pos > b.within_layer_pos
259    }
260}
261
262/// Internal merge-walk state. Tracks where one candidate
263/// override sits in the layer/position lattice.
264struct Candidate<'a> {
265    ov: &'a OptionOverride,
266    /// Higher = more authoritative. Caller iterates highest
267    /// priority first; encoded as `usize::MAX - layer_idx`.
268    layer_rank: usize,
269    /// Within-layer position; ties within a layer resolve to
270    /// higher position (= last pushed).
271    within_layer_pos: usize,
272    /// The layer this candidate came from; written to
273    /// [`ResolvedOptions`] alongside the value when this
274    /// candidate wins.
275    origin: OptionOrigin,
276}
277
278#[cfg(test)]
279mod tests {
280    #![allow(clippy::unwrap_used, clippy::panic)]
281
282    use super::*;
283    use crate::option_decl::{HasGroup, OptionDecl};
284    use std::any::TypeId;
285    use std::sync::Arc;
286
287    struct Tabstop;
288    impl OptionDecl for Tabstop {
289        type Value = i64;
290        const NAME: &'static str = "test-tabstop";
291        const DOC: &'static str = "";
292        fn default_value() -> i64 {
293            8
294        }
295    }
296    impl HasGroup for Tabstop {
297        const GROUP_NAME: &'static str = "editor";
298    }
299
300    struct Number;
301    impl OptionDecl for Number {
302        type Value = bool;
303        const NAME: &'static str = "test-number";
304        const DOC: &'static str = "";
305        fn default_value() -> bool {
306            false
307        }
308    }
309    impl HasGroup for Number {
310        const GROUP_NAME: &'static str = "editor";
311    }
312
313    fn ts(v: i64) -> OptionOverride {
314        OptionOverride::new(TypeId::of::<Tabstop>(), v)
315    }
316    fn ts_with(v: i64, p: OverridePriority) -> OptionOverride {
317        OptionOverride::with_priority(TypeId::of::<Tabstop>(), v, p)
318    }
319    fn num(v: bool) -> OptionOverride {
320        OptionOverride::new(TypeId::of::<Number>(), v)
321    }
322
323    fn read_i64(r: &ResolvedOptions, _t: &Tabstop) -> Option<i64> {
324        r.get::<Tabstop>().as_deref().copied()
325    }
326
327    fn read_bool(r: &ResolvedOptions, _t: &Number) -> Option<bool> {
328        r.get::<Number>().as_deref().copied()
329    }
330
331    #[test]
332    fn empty_layers_leave_cache_untouched() {
333        let resolver = Resolver::new();
334        let mut out = ResolvedOptions::new();
335        out.insert::<Tabstop>(8); // pretend the default was bootstrapped
336        let layers: [&OptionOverrideSet; 0] = [];
337        resolver.resolve_into(layers, &mut out);
338        assert_eq!(read_i64(&out, &Tabstop), Some(8));
339    }
340
341    #[test]
342    fn higher_priority_layer_wins() {
343        // Three layers; top is most authoritative.
344        let modal = OptionOverrideSet::from_iter([ts(1)]);
345        let buffer_local = OptionOverrideSet::from_iter([ts(2)]);
346        let global = OptionOverrideSet::from_iter([ts(3)]);
347        let resolver = Resolver::new();
348        let mut out = ResolvedOptions::new();
349        resolver.resolve_into([&modal, &buffer_local, &global], &mut out);
350        assert_eq!(read_i64(&out, &Tabstop), Some(1));
351    }
352
353    #[test]
354    fn within_a_layer_last_wins() {
355        let layer = OptionOverrideSet::from_iter([ts(1), ts(2), ts(3)]);
356        let resolver = Resolver::new();
357        let mut out = ResolvedOptions::new();
358        resolver.resolve_into([&layer], &mut out);
359        assert_eq!(read_i64(&out, &Tabstop), Some(3));
360    }
361
362    #[test]
363    fn high_priority_beats_higher_layer() {
364        // Modal layer (highest) at Normal vs minor layer (lower)
365        // at High: High wins despite being in a lower layer.
366        let modal = OptionOverrideSet::from_iter([ts(1)]);
367        let minor = OptionOverrideSet::from_iter([ts_with(99, OverridePriority::High)]);
368        let resolver = Resolver::new();
369        let mut out = ResolvedOptions::new();
370        resolver.resolve_into([&modal, &minor], &mut out);
371        assert_eq!(read_i64(&out, &Tabstop), Some(99));
372    }
373
374    #[test]
375    fn low_priority_loses_to_normal() {
376        let low = OptionOverrideSet::from_iter([ts_with(1, OverridePriority::Low)]);
377        let normal = OptionOverrideSet::from_iter([ts(2)]);
378        let resolver = Resolver::new();
379        let mut out = ResolvedOptions::new();
380        resolver.resolve_into([&low, &normal], &mut out);
381        assert_eq!(read_i64(&out, &Tabstop), Some(2));
382    }
383
384    #[test]
385    fn distinct_options_resolve_independently() {
386        let layer1 = OptionOverrideSet::from_iter([ts(4)]);
387        let layer2 = OptionOverrideSet::from_iter([num(true)]);
388        let resolver = Resolver::new();
389        let mut out = ResolvedOptions::new();
390        resolver.resolve_into([&layer1, &layer2], &mut out);
391        assert_eq!(read_i64(&out, &Tabstop), Some(4));
392        assert_eq!(read_bool(&out, &Number), Some(true));
393    }
394
395    #[test]
396    fn pre_populated_default_overridden_by_layer() {
397        // Bootstrap with default, then a layer overrides.
398        let mut out = ResolvedOptions::new();
399        out.insert::<Tabstop>(8);
400        let layer = OptionOverrideSet::from_iter([ts(2)]);
401        let resolver = Resolver::new();
402        resolver.resolve_into([&layer], &mut out);
403        assert_eq!(read_i64(&out, &Tabstop), Some(2));
404    }
405
406    #[test]
407    fn pre_populated_default_preserved_when_no_layer_covers() {
408        // Bootstrap default for Tabstop; layer only covers Number.
409        let mut out = ResolvedOptions::new();
410        out.insert::<Tabstop>(8);
411        let layer = OptionOverrideSet::from_iter([num(true)]);
412        let resolver = Resolver::new();
413        resolver.resolve_into([&layer], &mut out);
414        assert_eq!(read_i64(&out, &Tabstop), Some(8));
415        assert_eq!(read_bool(&out, &Number), Some(true));
416    }
417
418    #[test]
419    #[allow(unused_variables)] // suppress warning for type-arg-only references
420    fn arc_clone_round_trips() {
421        // Ensure ResolvedOptions::get returns Arc<T::Value>
422        // and the value survives clone semantics.
423        let mut out = ResolvedOptions::new();
424        out.insert::<Tabstop>(4);
425        let a: Arc<i64> = out.get::<Tabstop>().unwrap();
426        let b: Arc<i64> = out.get::<Tabstop>().unwrap();
427        assert_eq!(*a, 4);
428        assert_eq!(*b, 4);
429    }
430}