Skip to main content

lattice_config/
resolved.rs

1//! `ResolvedOptions`: per-buffer cached snapshot of every option's
2//! current resolved value (`mode-architecture.md` §6.3).
3//!
4//! Reads are O(1) `TypeId` lookups against a [`HashMap`]; the
5//! resolver populates the cache once per invalidation cycle.
6//! No layer walk on the keystroke path.
7//!
8//! Storage: erased through `Arc<dyn Any + Send + Sync>` so
9//! options of different `Value` types coexist in the same map.
10//! Typed reads downcast to the option's `Value` type at access.
11//! The downcast is infallible after a successful resolution
12//! (the resolver constructs entries with the correct type) but
13//! we still return `Option<T>` from the typed read because (a)
14//! the option may not be registered, and (b) the Rust API
15//! convention for "could be missing" is `Option`, not panic.
16
17use std::any::{Any, TypeId};
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use crate::option_decl::OptionDecl;
22use crate::origin::OptionOrigin;
23
24/// Cached snapshot of the resolved value for every option a
25/// buffer reads. Built by the [`crate::Resolver`]; invalidated
26/// on mode toggle, option write, or modal-state transition;
27/// recomputed eagerly on invalidation per the v1 invalidation
28/// policy (`mode-architecture.md` §6.3.1).
29///
30/// Public read API: [`Self::get`] (typed), [`Self::get_origin`]
31/// (layer that supplied the value). The internal storage is
32/// `pub(crate)` so the resolver in this crate can populate it;
33/// external code reads through the typed accessors.
34#[derive(Debug, Default, Clone)]
35pub struct ResolvedOptions {
36    by_type: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
37    /// Parallel map: which layer each option's winning value came from.
38    /// Entries are inserted in lockstep with `by_type`; missing entries
39    /// default to [`OptionOrigin::Default`].
40    origins: HashMap<TypeId, OptionOrigin>,
41}
42
43impl ResolvedOptions {
44    /// An empty cache. Every [`Self::get`] returns `None` until it is
45    /// seeded (typically by
46    /// [`crate::ConfigRegistry::bootstrap_resolved_with_current_values`])
47    /// and overlaid by a [`crate::Resolver`].
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Read the resolved value for option type `T`. Returns
53    /// `None` if `T` was not part of this resolution cycle
54    /// (e.g. the option isn't registered, or the resolver
55    /// hasn't run yet).
56    pub fn get<T: OptionDecl>(&self) -> Option<Arc<T::Value>>
57    where
58        T::Value: Send + Sync + 'static,
59    {
60        let any = self.by_type.get(&TypeId::of::<T>())?;
61        // The resolver always inserts `Arc<T::Value>`; this
62        // downcast is infallible by construction. We still
63        // return Option to avoid panicking when an option is
64        // unregistered (legitimate transient state during
65        // crate boot).
66        any.clone().downcast::<T::Value>().ok()
67    }
68
69    /// Test helper: insert a resolved value directly. Used by
70    /// tests across crates that exercise the read path without
71    /// running the resolver. Production code fills the cache through
72    /// the [`crate::Resolver`] and
73    /// [`crate::ConfigRegistry::bootstrap_resolved_with_current_values`],
74    /// which also record an [`OptionOrigin`]; this does not, so the
75    /// entry's origin stays whatever it was (default:
76    /// [`OptionOrigin::Default`]).
77    pub fn insert<T: OptionDecl>(&mut self, value: T::Value)
78    where
79        T::Value: Send + Sync + 'static,
80    {
81        self.by_type.insert(TypeId::of::<T>(), Arc::new(value));
82    }
83
84    /// Erased insert with explicit origin. Used by the resolver's
85    /// origin-aware path and by `bootstrap_resolved_with_current_values`.
86    pub(crate) fn insert_erased_with_origin(
87        &mut self,
88        type_id: TypeId,
89        value: Arc<dyn Any + Send + Sync>,
90        origin: OptionOrigin,
91    ) {
92        self.by_type.insert(type_id, value);
93        self.origins.insert(type_id, origin);
94    }
95
96    /// Look up the origin for option type `T`. Returns
97    /// [`OptionOrigin::Default`] if the option wasn't resolved in this
98    /// cycle (unknown option / resolver hasn't run yet).
99    pub fn get_origin<T: OptionDecl>(&self) -> OptionOrigin {
100        self.origins
101            .get(&TypeId::of::<T>())
102            .cloned()
103            .unwrap_or_default()
104    }
105
106    /// TypeId-keyed origin lookup for sites that only have a runtime
107    /// `TypeId` (e.g. the query echo path in `do_set`).
108    pub fn get_origin_for_typeid(&self, type_id: TypeId) -> OptionOrigin {
109        self.origins.get(&type_id).cloned().unwrap_or_default()
110    }
111
112    /// TypeId-keyed erased value lookup. Used by the query echo path
113    /// to format the resolved value when the concrete type isn't
114    /// statically known.
115    pub fn get_erased(&self, type_id: TypeId) -> Option<&Arc<dyn Any + Send + Sync>> {
116        self.by_type.get(&type_id)
117    }
118
119    /// Number of entries (for tests and `:describe-buffer` /
120    /// introspection diagnostic output).
121    pub fn len(&self) -> usize {
122        self.by_type.len()
123    }
124
125    /// `true` when no option has been resolved into the cache.
126    pub fn is_empty(&self) -> bool {
127        self.by_type.is_empty()
128    }
129
130    /// Iterate over `(TypeId, erased Arc)` pairs. Used by
131    /// `:describe-option-resolution` (M.8) and tests.
132    pub fn iter(&self) -> impl Iterator<Item = (&TypeId, &Arc<dyn Any + Send + Sync>)> {
133        self.by_type.iter()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    #![allow(clippy::unwrap_used, clippy::panic)]
140
141    use super::*;
142    use crate::option_decl::HasGroup;
143
144    struct Tabstop;
145    impl OptionDecl for Tabstop {
146        type Value = i64;
147        const NAME: &'static str = "test-tabstop";
148        const DOC: &'static str = "";
149        fn default_value() -> i64 {
150            8
151        }
152    }
153    impl HasGroup for Tabstop {
154        const GROUP_NAME: &'static str = "editor";
155    }
156
157    struct Number;
158    impl OptionDecl for Number {
159        type Value = bool;
160        const NAME: &'static str = "test-number";
161        const DOC: &'static str = "";
162        fn default_value() -> bool {
163            false
164        }
165    }
166    impl HasGroup for Number {
167        const GROUP_NAME: &'static str = "editor";
168    }
169
170    #[test]
171    fn empty_returns_none() {
172        let r = ResolvedOptions::new();
173        assert!(r.get::<Tabstop>().is_none());
174        assert!(r.is_empty());
175    }
176
177    #[test]
178    fn insert_and_get_round_trips() {
179        let mut r = ResolvedOptions::new();
180        r.insert::<Tabstop>(4);
181        let v = r.get::<Tabstop>().unwrap();
182        assert_eq!(*v, 4);
183    }
184
185    #[test]
186    fn distinct_options_coexist() {
187        let mut r = ResolvedOptions::new();
188        r.insert::<Tabstop>(2);
189        r.insert::<Number>(true);
190        assert_eq!(*r.get::<Tabstop>().unwrap(), 2);
191        assert!(*r.get::<Number>().unwrap());
192        assert_eq!(r.len(), 2);
193    }
194
195    #[test]
196    fn second_insert_for_same_type_overwrites() {
197        let mut r = ResolvedOptions::new();
198        r.insert::<Tabstop>(2);
199        r.insert::<Tabstop>(4);
200        assert_eq!(*r.get::<Tabstop>().unwrap(), 4);
201        assert_eq!(r.len(), 1);
202    }
203}