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 pub fn register(&self, spec: OptionSpec) -> Result<(), OptionError> {
128 let name = spec.name.to_string();
129
130 // Check for duplicate name first
131 if self.specs.read().contains_key(&name) {
132 return Err(OptionError::AlreadyExists(name));
133 }
134
135 // Handle alias registration if present
136 if let Some(ref short) = spec.short_form {
137 let short_str = short.to_string();
138
139 // Alias conflicts with existing full name
140 if self.specs.read().contains_key(&short_str) {
141 return Err(OptionError::AliasConflict(short_str));
142 }
143
144 // Alias conflicts with existing alias
145 if self.aliases.read().contains_key(&short_str) {
146 return Err(OptionError::AliasConflict(short_str));
147 }
148
149 // Register the alias
150 self.aliases.write().insert(short_str, name.clone());
151 }
152
153 // Finally insert the spec
154 self.specs.write().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;