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}