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}