mod constraint;
mod error;
mod scope;
mod spec;
mod value;
pub use {
constraint::{ConstraintError, OptionConstraint},
error::{OptionError, SetResult},
scope::{OptionScope, OptionScopeId},
spec::OptionSpec,
value::OptionValue,
};
use std::collections::HashMap;
use reovim_arch::sync::RwLock;
use crate::{
api::ModuleId,
mm::{BufferId, WindowId},
};
#[derive(Debug, Default)]
pub struct OptionRegistry {
specs: RwLock<HashMap<String, OptionSpec>>,
aliases: RwLock<HashMap<String, String>>,
global_values: RwLock<HashMap<String, OptionValue>>,
buffer_values: RwLock<HashMap<(BufferId, String), OptionValue>>,
window_values: RwLock<HashMap<(WindowId, String), OptionValue>>,
}
impl OptionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::significant_drop_tightening)] pub fn register(&self, spec: OptionSpec) -> Result<(), OptionError> {
let name = spec.name.to_string();
let mut specs = self.specs.write();
if specs.contains_key(&name) {
return Err(OptionError::AlreadyExists(name));
}
if let Some(ref short) = spec.short_form {
let short_str = short.to_string();
if specs.contains_key(&short_str) {
return Err(OptionError::AliasConflict(short_str));
}
let mut aliases = self.aliases.write();
if aliases.contains_key(&short_str) {
return Err(OptionError::AliasConflict(short_str));
}
aliases.insert(short_str, name.clone());
}
specs.insert(name, spec);
Ok(())
}
pub fn unregister_by_module(&self, module_id: &ModuleId) {
let names_to_remove: Vec<String> = self
.specs
.read()
.iter()
.filter(|(_, spec)| spec.owner.as_ref() == Some(module_id))
.map(|(name, _)| name.clone())
.collect();
if names_to_remove.is_empty() {
return;
}
let mut aliases = self.aliases.write();
aliases.retain(|_, full_name| !names_to_remove.contains(full_name));
drop(aliases);
let mut global_values = self.global_values.write();
global_values.retain(|name, _| !names_to_remove.contains(name));
drop(global_values);
let mut buffer_values = self.buffer_values.write();
buffer_values.retain(|(_, name), _| !names_to_remove.contains(name));
drop(buffer_values);
let mut window_values = self.window_values.write();
window_values.retain(|(_, name), _| !names_to_remove.contains(name));
drop(window_values);
let mut specs = self.specs.write();
specs.retain(|name, _| !names_to_remove.contains(name));
}
#[must_use]
pub fn list_by_module(&self, module_id: &ModuleId) -> Vec<OptionSpec> {
let specs = self.specs.read();
specs
.values()
.filter(|s| s.owner.as_ref() == Some(module_id))
.cloned()
.collect()
}
#[must_use]
pub fn resolve_name(&self, name: &str) -> Option<String> {
if self.specs.read().contains_key(name) {
return Some(name.to_string());
}
self.aliases.read().get(name).cloned()
}
#[must_use]
pub fn get_spec(&self, name: &str) -> Option<OptionSpec> {
let full_name = self.resolve_name(name)?;
let specs = self.specs.read();
specs.get(&full_name).cloned()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.resolve_name(name).is_some()
}
#[must_use]
pub fn get(&self, name: &str, scope: OptionScopeId) -> Option<OptionValue> {
let full_name = self.resolve_name(name)?;
let default_value = self.specs.read().get(&full_name)?.default.clone();
match scope {
OptionScopeId::Window(window_id) => {
if let Some(value) = self
.window_values
.read()
.get(&(window_id, full_name.clone()))
{
return Some(value.clone());
}
}
OptionScopeId::Buffer(buffer_id) => {
if let Some(value) = self
.buffer_values
.read()
.get(&(buffer_id, full_name.clone()))
{
return Some(value.clone());
}
}
OptionScopeId::Global => {}
}
if let Some(value) = self.global_values.read().get(&full_name) {
return Some(value.clone());
}
Some(default_value)
}
#[must_use]
pub fn get_global(&self, name: &str) -> Option<OptionValue> {
self.get(name, OptionScopeId::Global)
}
#[must_use]
pub fn get_for_buffer(&self, name: &str, buffer_id: BufferId) -> Option<OptionValue> {
self.get(name, OptionScopeId::Buffer(buffer_id))
}
#[must_use]
pub fn get_for_window(&self, name: &str, window_id: WindowId) -> Option<OptionValue> {
self.get(name, OptionScopeId::Window(window_id))
}
pub fn set(
&self,
name: &str,
value: OptionValue,
scope: OptionScopeId,
) -> Result<SetResult, OptionError> {
let full_name = self
.resolve_name(name)
.ok_or_else(|| OptionError::NotFound(name.to_string()))?;
let specs = self.specs.read();
let spec = specs
.get(&full_name)
.ok_or_else(|| OptionError::NotFound(full_name.clone()))?;
spec.validate(&value)?;
if matches!(
(&scope, &spec.scope),
(OptionScopeId::Buffer(_) | OptionScopeId::Window(_), OptionScope::Global)
) {
return Err(OptionError::ScopeMismatch {
name: full_name,
option_scope: spec.scope,
requested: scope,
});
}
drop(specs);
let old_value = match scope {
OptionScopeId::Global => {
let mut global_values = self.global_values.write();
global_values.insert(full_name, value.clone())
}
OptionScopeId::Buffer(buffer_id) => {
let mut buffer_values = self.buffer_values.write();
buffer_values.insert((buffer_id, full_name), value.clone())
}
OptionScopeId::Window(window_id) => {
let mut window_values = self.window_values.write();
window_values.insert((window_id, full_name), value.clone())
}
};
Ok(SetResult {
old_value,
new_value: value,
})
}
pub fn set_global(&self, name: &str, value: OptionValue) -> Result<SetResult, OptionError> {
self.set(name, value, OptionScopeId::Global)
}
pub fn set_for_buffer(
&self,
name: &str,
value: OptionValue,
buffer_id: BufferId,
) -> Result<SetResult, OptionError> {
self.set(name, value, OptionScopeId::Buffer(buffer_id))
}
pub fn set_for_window(
&self,
name: &str,
value: OptionValue,
window_id: WindowId,
) -> Result<SetResult, OptionError> {
self.set(name, value, OptionScopeId::Window(window_id))
}
pub fn reset(
&self,
name: &str,
scope: OptionScopeId,
) -> Result<Option<OptionValue>, OptionError> {
let full_name = self
.resolve_name(name)
.ok_or_else(|| OptionError::NotFound(name.to_string()))?;
let removed = match scope {
OptionScopeId::Global => {
let mut global_values = self.global_values.write();
global_values.remove(&full_name)
}
OptionScopeId::Buffer(buffer_id) => {
let mut buffer_values = self.buffer_values.write();
buffer_values.remove(&(buffer_id, full_name))
}
OptionScopeId::Window(window_id) => {
let mut window_values = self.window_values.write();
window_values.remove(&(window_id, full_name))
}
};
Ok(removed)
}
pub fn clear_buffer(&self, buffer_id: BufferId) {
let mut buffer_values = self.buffer_values.write();
buffer_values.retain(|(bid, _), _| *bid != buffer_id);
}
pub fn clear_window(&self, window_id: WindowId) {
let mut window_values = self.window_values.write();
window_values.retain(|(wid, _), _| *wid != window_id);
}
pub fn toggle(&self, name: &str, scope: OptionScopeId) -> Result<bool, OptionError> {
let current = self
.get(name, scope)
.ok_or_else(|| OptionError::NotFound(name.to_string()))?;
let current_bool = current.as_bool().ok_or_else(|| OptionError::TypeMismatch {
name: name.to_string(),
expected: "bool",
got: current.type_name(),
})?;
let new_value = !current_bool;
self.set(name, OptionValue::Bool(new_value), scope)?;
Ok(new_value)
}
#[must_use]
pub fn list_all(&self) -> Vec<String> {
let specs = self.specs.read();
specs.keys().cloned().collect()
}
#[must_use]
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn list_matching(&self, prefix: &str) -> Vec<OptionSpec> {
let specs = self.specs.read();
let mut results = Vec::new();
for (name, spec) in specs.iter() {
if name.starts_with(prefix) {
results.push(spec.clone());
}
}
let aliases = self.aliases.read();
for (alias, full_name) in aliases.iter() {
if alias.starts_with(prefix)
&& let Some(spec) = specs.get(full_name)
&& !results.iter().any(|s| s.name == spec.name)
{
results.push(spec.clone());
}
}
drop(aliases);
results
}
#[must_use]
pub fn list_by_scope(&self, scope: OptionScope) -> Vec<OptionSpec> {
let specs = self.specs.read();
specs
.values()
.filter(|s| s.scope == scope)
.cloned()
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
let specs = self.specs.read();
specs.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
let specs = self.specs.read();
specs.is_empty()
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;