Skip to main content

reovim_kernel/core/option/
mod.rs

1//! Option registry mechanism for editor settings.
2//!
3//! Linux equivalent: `/proc/sys/` configuration interface
4//!
5//! This module provides the **MECHANISM** for option registration, storage,
6//! and retrieval. **POLICY** (which options exist, what they do) is defined
7//! by modules.
8//!
9//! # Design Philosophy
10//!
11//! Following "mechanism, not policy":
12//! - Kernel provides `OptionRegistry` with type-safe value storage
13//! - Modules register `OptionSpec` definitions during initialization
14//! - Scope-aware storage (Global, Buffer, Window) with automatic fallback
15//!
16//! # Scope Resolution
17//!
18//! When getting an option value, the registry checks:
19//! 1. Window-local value (if scope is Window)
20//! 2. Buffer-local value (if scope is Buffer or Window)
21//! 3. Global override value
22//! 4. Default value from spec
23//!
24//! # Example
25//!
26//! ```ignore
27//! use reovim_kernel::api::v1::*;
28//!
29//! let registry = OptionRegistry::new();
30//!
31//! // Register an option (done by modules)
32//! registry.register(
33//!     OptionSpec::new("number", "Show line numbers", OptionValue::Bool(false))
34//!         .with_short("nu")
35//!         .with_scope(OptionScope::Window)
36//! )?;
37//!
38//! // Get/set values
39//! registry.set("number", OptionValue::Bool(true), OptionScopeId::Global)?;
40//! let value = registry.get("number", OptionScopeId::Global);
41//! ```
42
43mod constraint;
44mod error;
45mod scope;
46mod spec;
47mod value;
48
49// Re-export all types for API compatibility
50pub use {
51    constraint::{ConstraintError, OptionConstraint},
52    error::{OptionError, SetResult},
53    scope::{OptionScope, OptionScopeId},
54    spec::OptionSpec,
55    value::OptionValue,
56};
57
58use std::collections::HashMap;
59
60use reovim_arch::sync::RwLock;
61
62use crate::{
63    api::ModuleId,
64    mm::{BufferId, WindowId},
65};
66
67// ============================================================================
68// OptionRegistry - Thread-safe option storage
69// ============================================================================
70
71/// Thread-safe registry for all editor options.
72///
73/// This is the **MECHANISM** for option storage.
74/// **POLICY** (which options to register) is in modules.
75///
76/// # Storage Model
77///
78/// ```text
79/// OptionRegistry
80/// ├── specs: HashMap<String, OptionSpec>              # Option definitions
81/// ├── aliases: HashMap<String, String>                # Short -> Full name
82/// ├── global_values: HashMap<String, OptionValue>     # Global overrides
83/// ├── buffer_values: HashMap<(BufferId, String), OptionValue>   # Per-buffer
84/// └── window_values: HashMap<(WindowId, String), OptionValue>   # Per-window
85/// ```
86///
87/// # Thread Safety
88///
89/// All operations use `RwLock` for thread-safe access.
90/// Multiple readers allowed, single writer for mutations.
91#[derive(Debug, Default)]
92pub struct OptionRegistry {
93    /// Registered option specs indexed by full name.
94    specs: RwLock<HashMap<String, OptionSpec>>,
95
96    /// Short name -> full name mapping.
97    aliases: RwLock<HashMap<String, String>>,
98
99    /// Global option values (overrides from defaults).
100    global_values: RwLock<HashMap<String, OptionValue>>,
101
102    /// Buffer-local option values.
103    buffer_values: RwLock<HashMap<(BufferId, String), OptionValue>>,
104
105    /// Window-local option values.
106    window_values: RwLock<HashMap<(WindowId, String), OptionValue>>,
107}
108
109impl OptionRegistry {
110    /// Create a new empty option registry.
111    #[must_use]
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    // ========================================================================
117    // Registration
118    // ========================================================================
119
120    /// Register an option specification.
121    ///
122    /// # Errors
123    ///
124    /// Returns error if:
125    /// - Option with same name already exists
126    /// - Short alias conflicts with existing name or alias
127    #[allow(clippy::significant_drop_tightening)] // intentional: hold lock for atomicity
128    pub fn register(&self, spec: OptionSpec) -> Result<(), OptionError> {
129        let name = spec.name.to_string();
130
131        // Hold write lock for entire check-and-insert to prevent TOCTOU race.
132        let mut specs = self.specs.write();
133
134        if specs.contains_key(&name) {
135            return Err(OptionError::AlreadyExists(name));
136        }
137
138        // Handle alias registration if present
139        if let Some(ref short) = spec.short_form {
140            let short_str = short.to_string();
141
142            if specs.contains_key(&short_str) {
143                return Err(OptionError::AliasConflict(short_str));
144            }
145
146            let mut aliases = self.aliases.write();
147            if aliases.contains_key(&short_str) {
148                return Err(OptionError::AliasConflict(short_str));
149            }
150
151            aliases.insert(short_str, name.clone());
152        }
153
154        specs.insert(name, spec);
155        Ok(())
156    }
157
158    /// Unregister all options owned by a module.
159    ///
160    /// Removes all option specs with matching owner, their aliases,
161    /// and any stored values (global, buffer-local, window-local).
162    pub fn unregister_by_module(&self, module_id: &ModuleId) {
163        // Collect names to remove
164        let names_to_remove: Vec<String> = self
165            .specs
166            .read()
167            .iter()
168            .filter(|(_, spec)| spec.owner.as_ref() == Some(module_id))
169            .map(|(name, _)| name.clone())
170            .collect();
171
172        if names_to_remove.is_empty() {
173            return;
174        }
175
176        // Remove aliases pointing to these options
177        let mut aliases = self.aliases.write();
178        aliases.retain(|_, full_name| !names_to_remove.contains(full_name));
179        drop(aliases);
180
181        // Remove stored values
182        let mut global_values = self.global_values.write();
183        global_values.retain(|name, _| !names_to_remove.contains(name));
184        drop(global_values);
185
186        let mut buffer_values = self.buffer_values.write();
187        buffer_values.retain(|(_, name), _| !names_to_remove.contains(name));
188        drop(buffer_values);
189
190        let mut window_values = self.window_values.write();
191        window_values.retain(|(_, name), _| !names_to_remove.contains(name));
192        drop(window_values);
193
194        // Remove specs
195        let mut specs = self.specs.write();
196        specs.retain(|name, _| !names_to_remove.contains(name));
197    }
198
199    /// List all options owned by a module.
200    #[must_use]
201    pub fn list_by_module(&self, module_id: &ModuleId) -> Vec<OptionSpec> {
202        let specs = self.specs.read();
203        specs
204            .values()
205            .filter(|s| s.owner.as_ref() == Some(module_id))
206            .cloned()
207            .collect()
208    }
209
210    // ========================================================================
211    // Name Resolution
212    // ========================================================================
213
214    /// Resolve a name (which may be an alias) to the full option name.
215    #[must_use]
216    pub fn resolve_name(&self, name: &str) -> Option<String> {
217        // Check if it's a direct spec name
218        if self.specs.read().contains_key(name) {
219            return Some(name.to_string());
220        }
221
222        // Check if it's an alias
223        self.aliases.read().get(name).cloned()
224    }
225
226    /// Get an option specification by name (supports aliases).
227    #[must_use]
228    pub fn get_spec(&self, name: &str) -> Option<OptionSpec> {
229        let full_name = self.resolve_name(name)?;
230        let specs = self.specs.read();
231        specs.get(&full_name).cloned()
232    }
233
234    /// Check if an option exists.
235    #[must_use]
236    pub fn contains(&self, name: &str) -> bool {
237        self.resolve_name(name).is_some()
238    }
239
240    // ========================================================================
241    // Value Access - Scope-aware
242    // ========================================================================
243
244    /// Get the effective value of an option for a given scope.
245    ///
246    /// Resolution order (most specific wins):
247    /// 1. Window-local value (if `scope` is `Window`)
248    /// 2. Buffer-local value (if `scope` is `Buffer` or has buffer context)
249    /// 3. Global value (if set)
250    /// 4. Default value from spec
251    #[must_use]
252    pub fn get(&self, name: &str, scope: OptionScopeId) -> Option<OptionValue> {
253        let full_name = self.resolve_name(name)?;
254
255        // Get default value from spec first, then release lock
256        let default_value = self.specs.read().get(&full_name)?.default.clone();
257
258        // Check scope-specific values
259        match scope {
260            OptionScopeId::Window(window_id) => {
261                // Check window-local first
262                if let Some(value) = self
263                    .window_values
264                    .read()
265                    .get(&(window_id, full_name.clone()))
266                {
267                    return Some(value.clone());
268                }
269            }
270            OptionScopeId::Buffer(buffer_id) => {
271                // Check buffer-local
272                if let Some(value) = self
273                    .buffer_values
274                    .read()
275                    .get(&(buffer_id, full_name.clone()))
276                {
277                    return Some(value.clone());
278                }
279            }
280            OptionScopeId::Global => {}
281        }
282
283        // Check global override
284        if let Some(value) = self.global_values.read().get(&full_name) {
285            return Some(value.clone());
286        }
287
288        // Return default
289        Some(default_value)
290    }
291
292    /// Get global value (ignoring scope context).
293    #[must_use]
294    pub fn get_global(&self, name: &str) -> Option<OptionValue> {
295        self.get(name, OptionScopeId::Global)
296    }
297
298    /// Get buffer-local value.
299    #[must_use]
300    pub fn get_for_buffer(&self, name: &str, buffer_id: BufferId) -> Option<OptionValue> {
301        self.get(name, OptionScopeId::Buffer(buffer_id))
302    }
303
304    /// Get window-local value.
305    #[must_use]
306    pub fn get_for_window(&self, name: &str, window_id: WindowId) -> Option<OptionValue> {
307        self.get(name, OptionScopeId::Window(window_id))
308    }
309
310    // ========================================================================
311    // Value Setting
312    // ========================================================================
313
314    /// Set an option value at a specific scope.
315    ///
316    /// # Errors
317    ///
318    /// Returns error if:
319    /// - Option not found
320    /// - Value fails validation
321    /// - Scope mismatch
322    pub fn set(
323        &self,
324        name: &str,
325        value: OptionValue,
326        scope: OptionScopeId,
327    ) -> Result<SetResult, OptionError> {
328        let full_name = self
329            .resolve_name(name)
330            .ok_or_else(|| OptionError::NotFound(name.to_string()))?;
331
332        let specs = self.specs.read();
333        let spec = specs
334            .get(&full_name)
335            .ok_or_else(|| OptionError::NotFound(full_name.clone()))?;
336
337        // Validate value
338        spec.validate(&value)?;
339
340        // Check scope compatibility: only global-scoped options cannot have local overrides
341        if matches!(
342            (&scope, &spec.scope),
343            (OptionScopeId::Buffer(_) | OptionScopeId::Window(_), OptionScope::Global)
344        ) {
345            return Err(OptionError::ScopeMismatch {
346                name: full_name,
347                option_scope: spec.scope,
348                requested: scope,
349            });
350        }
351
352        drop(specs); // Release read lock before acquiring write lock
353
354        // Set the value
355        let old_value = match scope {
356            OptionScopeId::Global => {
357                let mut global_values = self.global_values.write();
358                global_values.insert(full_name, value.clone())
359            }
360            OptionScopeId::Buffer(buffer_id) => {
361                let mut buffer_values = self.buffer_values.write();
362                buffer_values.insert((buffer_id, full_name), value.clone())
363            }
364            OptionScopeId::Window(window_id) => {
365                let mut window_values = self.window_values.write();
366                window_values.insert((window_id, full_name), value.clone())
367            }
368        };
369
370        Ok(SetResult {
371            old_value,
372            new_value: value,
373        })
374    }
375
376    /// Set global value.
377    ///
378    /// # Errors
379    ///
380    /// Returns error if option not found or validation fails.
381    pub fn set_global(&self, name: &str, value: OptionValue) -> Result<SetResult, OptionError> {
382        self.set(name, value, OptionScopeId::Global)
383    }
384
385    /// Set buffer-local value.
386    ///
387    /// # Errors
388    ///
389    /// Returns error if option not found, validation fails, or scope mismatch.
390    pub fn set_for_buffer(
391        &self,
392        name: &str,
393        value: OptionValue,
394        buffer_id: BufferId,
395    ) -> Result<SetResult, OptionError> {
396        self.set(name, value, OptionScopeId::Buffer(buffer_id))
397    }
398
399    /// Set window-local value.
400    ///
401    /// # Errors
402    ///
403    /// Returns error if option not found, validation fails, or scope mismatch.
404    pub fn set_for_window(
405        &self,
406        name: &str,
407        value: OptionValue,
408        window_id: WindowId,
409    ) -> Result<SetResult, OptionError> {
410        self.set(name, value, OptionScopeId::Window(window_id))
411    }
412
413    // ========================================================================
414    // Reset
415    // ========================================================================
416
417    /// Reset an option to its default value at a specific scope.
418    ///
419    /// # Errors
420    ///
421    /// Returns error if option not found.
422    pub fn reset(
423        &self,
424        name: &str,
425        scope: OptionScopeId,
426    ) -> Result<Option<OptionValue>, OptionError> {
427        let full_name = self
428            .resolve_name(name)
429            .ok_or_else(|| OptionError::NotFound(name.to_string()))?;
430
431        let removed = match scope {
432            OptionScopeId::Global => {
433                let mut global_values = self.global_values.write();
434                global_values.remove(&full_name)
435            }
436            OptionScopeId::Buffer(buffer_id) => {
437                let mut buffer_values = self.buffer_values.write();
438                buffer_values.remove(&(buffer_id, full_name))
439            }
440            OptionScopeId::Window(window_id) => {
441                let mut window_values = self.window_values.write();
442                window_values.remove(&(window_id, full_name))
443            }
444        };
445
446        Ok(removed)
447    }
448
449    /// Reset all buffer-local values for a buffer (called when buffer closes).
450    pub fn clear_buffer(&self, buffer_id: BufferId) {
451        let mut buffer_values = self.buffer_values.write();
452        buffer_values.retain(|(bid, _), _| *bid != buffer_id);
453    }
454
455    /// Reset all window-local values for a window (called when window closes).
456    pub fn clear_window(&self, window_id: WindowId) {
457        let mut window_values = self.window_values.write();
458        window_values.retain(|(wid, _), _| *wid != window_id);
459    }
460
461    // ========================================================================
462    // Toggle
463    // ========================================================================
464
465    /// Toggle a boolean option.
466    ///
467    /// # Errors
468    ///
469    /// Returns error if option not found or is not boolean.
470    pub fn toggle(&self, name: &str, scope: OptionScopeId) -> Result<bool, OptionError> {
471        let current = self
472            .get(name, scope)
473            .ok_or_else(|| OptionError::NotFound(name.to_string()))?;
474
475        let current_bool = current.as_bool().ok_or_else(|| OptionError::TypeMismatch {
476            name: name.to_string(),
477            expected: "bool",
478            got: current.type_name(),
479        })?;
480
481        let new_value = !current_bool;
482        self.set(name, OptionValue::Bool(new_value), scope)?;
483        Ok(new_value)
484    }
485
486    // ========================================================================
487    // Query
488    // ========================================================================
489
490    /// List all registered option names.
491    #[must_use]
492    pub fn list_all(&self) -> Vec<String> {
493        let specs = self.specs.read();
494        specs.keys().cloned().collect()
495    }
496
497    /// List options matching a prefix (for tab completion).
498    #[must_use]
499    #[cfg_attr(coverage_nightly, coverage(off))]
500    pub fn list_matching(&self, prefix: &str) -> Vec<OptionSpec> {
501        let specs = self.specs.read();
502
503        let mut results = Vec::new();
504
505        for (name, spec) in specs.iter() {
506            if name.starts_with(prefix) {
507                results.push(spec.clone());
508            }
509        }
510
511        // Also check aliases
512        let aliases = self.aliases.read();
513        for (alias, full_name) in aliases.iter() {
514            if alias.starts_with(prefix)
515                && let Some(spec) = specs.get(full_name)
516                && !results.iter().any(|s| s.name == spec.name)
517            {
518                results.push(spec.clone());
519            }
520        }
521        drop(aliases);
522
523        results
524    }
525
526    /// List options by scope.
527    #[must_use]
528    pub fn list_by_scope(&self, scope: OptionScope) -> Vec<OptionSpec> {
529        let specs = self.specs.read();
530        specs
531            .values()
532            .filter(|s| s.scope == scope)
533            .cloned()
534            .collect()
535    }
536
537    /// Get the number of registered options.
538    #[must_use]
539    pub fn len(&self) -> usize {
540        let specs = self.specs.read();
541        specs.len()
542    }
543
544    /// Check if the registry is empty.
545    #[must_use]
546    pub fn is_empty(&self) -> bool {
547        let specs = self.specs.read();
548        specs.is_empty()
549    }
550}
551
552#[cfg(test)]
553#[path = "mod_tests.rs"]
554mod tests;