diff --git a/Cargo.lock b/Cargo.lock index a7e06b6ef..e97daba35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1047,6 +1047,19 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "helix-config" +version = "23.10.0" +dependencies = [ + "ahash", + "anyhow", + "hashbrown 0.14.3", + "indexmap", + "parking_lot", + "serde", + "serde_json", +] + [[package]] name = "helix-core" version = "23.10.0" @@ -1059,6 +1072,7 @@ dependencies = [ "encoding_rs", "etcetera", "hashbrown 0.14.3", + "helix-config", "helix-loader", "imara-diff", "indoc", @@ -1132,6 +1146,7 @@ dependencies = [ "futures-executor", "futures-util", "globset", + "helix-config", "helix-core", "helix-loader", "helix-parsec", diff --git a/Cargo.toml b/Cargo.toml index 6c006fbb4..77092ba78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "helix-core", + "helix-config", "helix-view", "helix-term", "helix-tui", diff --git a/helix-config/Cargo.toml b/helix-config/Cargo.toml new file mode 100644 index 000000000..ba9bbb5d8 --- /dev/null +++ b/helix-config/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "helix-config" +description = "Helix editor core editing primitives" +include = ["src/**/*", "README.md"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +categories.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +ahash = "0.8.6" +hashbrown = { version = "0.14.3", features = ["raw"] } +parking_lot = "0.12" +anyhow = "1.0.79" +indexmap = { version = "2.1.0", features = ["serde"] } +serde = { version = "1.0" } +serde_json = "1.0" + +regex-syntax = "0.8.2" +which = "5.0.0" diff --git a/helix-config/src/any.rs b/helix-config/src/any.rs new file mode 100644 index 000000000..891d9e8c5 --- /dev/null +++ b/helix-config/src/any.rs @@ -0,0 +1,76 @@ +/// this is a reimplementation of dynamic dispatch that only stores the +/// information we need and stores everythin inline. Values that are smaller or +/// the same size as a slice (2 usize) are also stored inline. This avoids +/// significant overallocation when setting lots of simple config +/// options (integers, strings, lists, enums) +use std::any::{Any, TypeId}; +use std::mem::{align_of, size_of, MaybeUninit}; + +pub struct ConfigData { + data: MaybeUninit<[usize; 2]>, + ty: TypeId, + drop_fn: unsafe fn(MaybeUninit<[usize; 2]>), +} + +const fn store_inline() -> bool { + size_of::() <= size_of::<[usize; 2]>() && align_of::() <= align_of::<[usize; 2]>() +} + +impl ConfigData { + unsafe fn drop_impl(mut data: MaybeUninit<[usize; 2]>) { + if store_inline::() { + data.as_mut_ptr().cast::().drop_in_place(); + } else { + let ptr = data.as_mut_ptr().cast::<*mut T>().read(); + drop(Box::from_raw(ptr)); + } + } + + pub fn get(&self) -> &T { + assert_eq!(TypeId::of::(), self.ty); + unsafe { + if store_inline::() { + return &*self.data.as_ptr().cast(); + } + let data: *const T = self.data.as_ptr().cast::<*const T>().read(); + &*data + } + } + pub fn new(val: T) -> Self { + let mut data = MaybeUninit::uninit(); + if store_inline::() { + let data: *mut T = data.as_mut_ptr() as _; + unsafe { + data.write(val); + } + } else { + assert!(store_inline::<*const T>()); + let data: *mut *const T = data.as_mut_ptr() as _; + unsafe { + data.write(Box::into_raw(Box::new(val))); + } + }; + Self { + data, + ty: TypeId::of::(), + drop_fn: ConfigData::drop_impl::, + } + } +} + +impl Drop for ConfigData { + fn drop(&mut self) { + unsafe { + (self.drop_fn)(self.data); + } + } +} + +impl std::fmt::Debug for ConfigData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfigData").finish_non_exhaustive() + } +} + +unsafe impl Send for ConfigData {} +unsafe impl Sync for ConfigData {} diff --git a/helix-config/src/convert.rs b/helix-config/src/convert.rs new file mode 100644 index 000000000..1ee3b6f74 --- /dev/null +++ b/helix-config/src/convert.rs @@ -0,0 +1,42 @@ +use crate::any::ConfigData; +use crate::validator::Ty; +use crate::Value; + +pub trait IntoTy: Clone { + type Ty: Ty; + fn into_ty(self) -> Self::Ty; +} + +impl IntoTy for T { + type Ty = Self; + + fn into_ty(self) -> Self::Ty { + self + } +} +impl IntoTy for &[T] { + type Ty = Box<[T::Ty]>; + + fn into_ty(self) -> Self::Ty { + self.iter().cloned().map(T::into_ty).collect() + } +} +impl IntoTy for &[T; N] { + type Ty = Box<[T::Ty]>; + + fn into_ty(self) -> Self::Ty { + self.iter().cloned().map(T::into_ty).collect() + } +} + +impl IntoTy for &str { + type Ty = Box; + + fn into_ty(self) -> Self::Ty { + self.into() + } +} + +pub(super) fn ty_into_value(val: &ConfigData) -> Value { + T::to_value(val.get()) +} diff --git a/helix-config/src/lib.rs b/helix-config/src/lib.rs new file mode 100644 index 000000000..8f27e41ec --- /dev/null +++ b/helix-config/src/lib.rs @@ -0,0 +1,242 @@ +use std::any::Any; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; +use std::sync::Arc; + +use anyhow::bail; +use hashbrown::hash_map::Entry; +use hashbrown::HashMap; +use indexmap::IndexMap; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; + +use any::ConfigData; +use convert::ty_into_value; +pub use convert::IntoTy; +pub use definition::init_config; +use validator::StaticValidator; +pub use validator::{regex_str_validator, ty_validator, IntegerRangeValidator, Ty, Validator}; +pub use value::{from_value, to_value, Value}; + +mod any; +mod convert; +mod macros; +mod validator; +mod value; + +pub type Guard<'a, T> = MappedRwLockReadGuard<'a, T>; +pub type Map = IndexMap, T, ahash::RandomState>; +pub type String = Box; +pub type List = Box<[T]>; + +#[cfg(test)] +mod tests; + +#[derive(Debug)] +pub struct OptionInfo { + pub name: Arc, + pub description: Box, + pub validator: Box, + pub into_value: fn(&ConfigData) -> Value, +} + +#[derive(Debug)] +pub struct OptionManager { + vals: RwLock, ConfigData>>, + parent: Option>, +} + +impl OptionManager { + pub fn get(&self, option: &str) -> Guard<'_, T> { + Guard::map(self.get_data(option), ConfigData::get) + } + + pub fn get_data(&self, option: &str) -> Guard<'_, ConfigData> { + let mut current_scope = self; + loop { + let lock = current_scope.vals.read(); + if let Ok(res) = RwLockReadGuard::try_map(lock, |options| options.get(option)) { + return res; + } + let Some(new_scope) = current_scope.parent.as_deref() else{ + unreachable!("option must be atleast defined in the global scope") + }; + current_scope = new_scope; + } + } + + pub fn get_deref(&self, option: &str) -> Guard<'_, T::Target> { + Guard::map(self.get::(option), T::deref) + } + + pub fn get_folded( + &self, + option: &str, + init: R, + mut fold: impl FnMut(&T, R) -> R, + ) -> R { + let mut res = init; + let mut current_scope = self; + loop { + let options = current_scope.vals.read(); + if let Some(option) = options.get(option).map(|val| val.get()) { + res = fold(option, res); + } + let Some(new_scope) = current_scope.parent.as_deref() else{ + break + }; + current_scope = new_scope; + } + res + } + + pub fn get_value( + &self, + option: impl Into>, + registry: &OptionRegistry, + ) -> anyhow::Result { + let option: Arc = option.into(); + let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") }; + let data = self.get_data(&option); + let val = (opt.into_value)(&data); + Ok(val) + } + + pub fn create_scope(self: &Arc) -> OptionManager { + OptionManager { + vals: RwLock::default(), + parent: Some(self.clone()), + } + } + + pub fn set_parent_scope(&mut self, parent: Arc) { + self.parent = Some(parent) + } + + pub fn set_unchecked(&self, option: Arc, val: ConfigData) { + self.vals.write().insert(option, val); + } + + pub fn append( + &self, + option: impl Into>, + val: impl Into, + registry: &OptionRegistry, + max_depth: usize, + ) -> anyhow::Result<()> { + let val = val.into(); + let option: Arc = option.into(); + let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") }; + let old_data = self.get_data(&option); + let mut old = (opt.into_value)(&old_data); + old.append(val, max_depth); + let val = opt.validator.validate(old)?; + self.set_unchecked(option, val); + Ok(()) + } + + /// Sets the value of a config option. Returns an error if this config + /// option doesn't exist or the provided value is not valid. + pub fn set( + &self, + option: impl Into>, + val: impl Into, + registry: &OptionRegistry, + ) -> anyhow::Result<()> { + let option: Arc = option.into(); + let val = val.into(); + let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") }; + let val = opt.validator.validate(val)?; + self.set_unchecked(option, val); + Ok(()) + } + + /// unsets an options so that its value will be read from + /// the parent scope instead + pub fn unset(&self, option: &str) { + self.vals.write().remove(option); + } +} + +#[derive(Debug)] +pub struct OptionRegistry { + options: HashMap, OptionInfo>, + defaults: Arc, +} + +impl OptionRegistry { + pub fn new() -> Self { + Self { + options: HashMap::with_capacity(1024), + defaults: Arc::new(OptionManager { + vals: RwLock::new(HashMap::with_capacity(1024)), + parent: None, + }), + } + } + + pub fn register(&mut self, name: &str, description: &str, default: T) { + self.register_with_validator( + name, + description, + default, + StaticValidator:: { ty: PhantomData }, + ); + } + + pub fn register_with_validator( + &mut self, + name: &str, + description: &str, + default: T, + validator: impl Validator, + ) { + let mut name: Arc = name.into(); + // convert from snake case to kebab case in place without an additional + // allocation this is save since we only replace ascii with ascii in + // place std really ougth to have a function for this :/ + // TODO: move to stdx as extension trait + for byte in unsafe { Arc::get_mut(&mut name).unwrap().as_bytes_mut() } { + if *byte == b'-' { + *byte = b'_'; + } + } + let default = default.into_ty(); + match self.options.entry(name.clone()) { + Entry::Vacant(e) => { + // make sure the validator is correct + if cfg!(debug_assertions) { + validator.validate(T::Ty::to_value(&default)).unwrap(); + } + let opt = OptionInfo { + name: name.clone(), + description: description.into(), + validator: Box::new(validator), + into_value: ty_into_value::, + }; + e.insert(opt); + } + Entry::Occupied(ent) => { + ent.get() + .validator + .validate(T::Ty::to_value(&default)) + .unwrap(); + } + } + self.defaults.set_unchecked(name, ConfigData::new(default)); + } + + pub fn global_scope(&self) -> Arc { + self.defaults.clone() + } + + pub fn get(&self, name: &str) -> Option<&OptionInfo> { + self.options.get(name) + } +} + +impl Default for OptionRegistry { + fn default() -> Self { + Self::new() + } +} diff --git a/helix-config/src/macros.rs b/helix-config/src/macros.rs new file mode 100644 index 000000000..69e9c75cc --- /dev/null +++ b/helix-config/src/macros.rs @@ -0,0 +1,130 @@ +/// This macro allows specifiying a trait of related config +/// options with a struct like syntax. From that information +/// two things are generated: +/// +/// * A `init_config` function that registers the config options with the +/// `OptionRegistry` registry. +/// * A **trait** definition with an accessor for every config option that is +/// implemented for `OptionManager`. +/// +/// The accessors on the trait allow convenient statically typed access to +/// config fields. The accessors return `Guard` (which allows derferecning to +/// &T). Any type that implements copy can be returned as a copy instead by +/// specifying `#[read = copy]`. Collections like `List` and `String` are not +/// copy However, they usually implement deref (to &[T] and &str respectively). +/// Working with the dereferneced &str/&[T] is more convenient then &String and &List. The +/// accessor will return these if `#[read = deref]` is specified. +/// +/// The doc comments will be retained for the accessors and also stored in the +/// option registrry for dispaly in the UI and documentation. +/// +/// The name of a config option can be changed with #[name = ""], +/// otherwise the name of the field is used directly. The OptionRegistry +/// automatically converts all names to kebab-case so a name attribute is only +/// required if the name is supposed to be significantly altered. +/// +/// In some cases more complex validation may be necssary. In that case the +/// valiidtator can be provided with an exprission that implements the `Validator` +/// trait: `#[validator = create_validator()]`. +#[macro_export] +macro_rules! options { + ( + $(use $use: ident::*;)* + $($(#[$($meta: tt)*])* struct $ident: ident { + $( + $(#[doc = $option_desc: literal])* + $(#[name = $option_name: literal])? + $(#[validator = $option_validator: expr])? + $(#[read = $($extra: tt)*])? + $option: ident: $ty: ty = $default: expr + ),+$(,)? + })+ + ) => { + $(pub use $use::*;)* + $($(#[$($meta)*])* pub trait $ident { + $( + $(#[doc = $option_desc])* + fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty); + )+ + })+ + pub fn init_config(registry: &mut $crate::OptionRegistry) { + $($use::init_config(registry);)* + $($( + let name = $crate::options!(@name $option $($option_name)?); + let docs = concat!("" $(,$option_desc,)" "*); + $crate::options!(@register registry name docs $default, $ty $(,$option_validator)?); + )+)+ + } + $(impl $ident for $crate::OptionManager { + $( + $(#[doc = $option_desc])* + fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty) { + let name = $crate::options!(@name $option $($option_name)?); + $crate::options!(@get $($($extra)*)? self, $ty, name) + } + )+ + })+ + }; + (@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty) => {{ + use $crate::IntoTy; + let val: $ty = $default.into_ty(); + $registry.register($name, $desc, val); + }}; + (@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty, $validator: expr) => {{ + use $crate::IntoTy; + let val: $ty = $default.into_ty(); + $registry.register_with_validator($name, $desc, val, $validator); + }}; + (@name $ident: ident) => { + ::std::stringify!($ident) + }; + (@name $ident: ident $name: literal) => { + $name + }; + (@ret_ty copy $ty: ty) => { + $ty + }; + (@ret_ty map($fn: expr, $ret_ty: ty) $ty: ty) => { + $ret_ty + }; + (@ret_ty fold($init: expr, $fn: expr, $ret_ty: ty) $ty: ty) => { + $ret_ty + }; + (@ret_ty deref $ty: ty) => { + $crate::Guard<'_, <$ty as ::std::ops::Deref>::Target> + }; + (@ret_ty $ty: ty) => { + $crate::Guard<'_, $ty> + }; + (@get map($fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => { + let val = $config.get::<$ty>($name); + $fn(val) + }; + (@get fold($init: expr, $fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => { + $config.get_folded::<$ty, $ret_ty>($name, $init, $fn) + }; + (@get copy $config: ident, $ty: ty, $name: ident) => { + *$config.get::<$ty>($name) + }; + (@get deref $config: ident, $ty: ty, $name: ident) => { + $config.get_deref::<$ty>($name) + }; + (@get $config: ident, $ty: ty, $name: ident) => { + $config.get::<$ty>($name) + }; +} + +#[macro_export] +macro_rules! config_serde_adapter { + ($ty: ident) => { + impl $crate::Ty for $ty { + fn to_value(&self) -> $crate::Value { + $crate::to_value(self).unwrap() + } + fn from_value(val: $crate::Value) -> ::anyhow::Result { + let val = $crate::from_value(val)?; + Ok(val) + } + } + }; +} diff --git a/helix-config/src/tests.rs b/helix-config/src/tests.rs new file mode 100644 index 000000000..6a724bd64 --- /dev/null +++ b/helix-config/src/tests.rs @@ -0,0 +1,80 @@ +use std::ops::Deref; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::config_serde_adapter; +use crate::OptionRegistry; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum LineNumber { + /// Show absolute line number + #[serde(alias = "abs")] + Absolute, + /// If focused and in normal/select mode, show relative line number to the primary cursor. + /// If unfocused or in insert mode, show absolute line number. + #[serde(alias = "rel")] + Relative, +} + +config_serde_adapter!(LineNumber); + +fn setup_registry() -> OptionRegistry { + let mut registry = OptionRegistry::new(); + registry.register( + "scrolloff", + "Number of lines of padding around the edge of the screen when scrolling", + 5usize, + ); + registry.register( + "shell", + "Shell to use when running external commands", + &["sh", "-c"], + ); + registry.register("mouse", "Enable mouse mode", true); + registry.register( + "line-number", + "Line number display: `absolute` simply shows each line's number, while \ + `relative` shows the distance from the current line. When unfocused or in \ + insert mode, `relative` will still show absolute line numbers", + LineNumber::Absolute, + ); + registry +} + +#[test] +fn default_values() { + let registry = setup_registry(); + let global_scope = registry.global_scope(); + let scrolloff: usize = *global_scope.get("scrolloff"); + let shell_ = global_scope.get_deref::>("shell"); + let shell: &[Box] = &shell_; + let mouse: bool = *global_scope.get("mouse"); + let line_number: LineNumber = *global_scope.get("line-number"); + assert_eq!(scrolloff, 5); + assert!(shell.iter().map(Box::deref).eq(["sh", "-c"])); + assert!(mouse); + assert_eq!(line_number, LineNumber::Absolute); +} + +#[test] +fn scope_overwrite() { + let registry = setup_registry(); + let global_scope = registry.global_scope(); + let scope_1 = Arc::new(global_scope.create_scope()); + let scope_2 = Arc::new(global_scope.create_scope()); + let mut scope_3 = scope_1.create_scope(); + scope_1.set("line-number", "rel", ®istry).unwrap(); + let line_number: LineNumber = *scope_3.get("line-number"); + assert_eq!(line_number, LineNumber::Relative); + scope_3.set_parent_scope(scope_2.clone()); + let line_number: LineNumber = *scope_3.get("line-number"); + assert_eq!(line_number, LineNumber::Absolute); + scope_2.set("line-number", "rel", ®istry).unwrap(); + let line_number: LineNumber = *scope_3.get("line-number"); + assert_eq!(line_number, LineNumber::Relative); + scope_2.set("line-number", "abs", ®istry).unwrap(); + let line_number: LineNumber = *scope_3.get("line-number"); + assert_eq!(line_number, LineNumber::Absolute); +} diff --git a/helix-config/src/validator.rs b/helix-config/src/validator.rs new file mode 100644 index 000000000..7c56c3587 --- /dev/null +++ b/helix-config/src/validator.rs @@ -0,0 +1,296 @@ +use std::any::{type_name, Any}; +use std::error::Error; +use std::fmt::Debug; +use std::marker::PhantomData; + +use anyhow::{bail, ensure, Result}; + +use crate::any::ConfigData; +use crate::Value; + +pub trait Validator: 'static + Debug { + fn validate(&self, val: Value) -> Result; +} + +pub trait Ty: Sized + Clone + 'static { + fn from_value(val: Value) -> Result; + fn to_value(&self) -> Value; +} + +#[derive(Clone, Copy)] +pub struct IntegerRangeValidator { + pub min: isize, + pub max: isize, + ty: PhantomData, +} +impl IntegerRangeValidator +where + E: Debug, + T: TryInto, +{ + pub fn new(min: T, max: T) -> Self { + Self { + min: min.try_into().unwrap(), + max: max.try_into().unwrap(), + ty: PhantomData, + } + } +} + +impl Debug for IntegerRangeValidator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IntegerRangeValidator") + .field("min", &self.min) + .field("max", &self.max) + .field("ty", &type_name::()) + .finish() + } +} + +impl IntegerRangeValidator +where + E: Error + Sync + Send + 'static, + T: Any + TryFrom, +{ + pub fn validate(&self, val: Value) -> Result { + let IntegerRangeValidator { min, max, .. } = *self; + let Value::Int(val) = val else { + bail!("expected an integer") + }; + ensure!( + min <= val && val <= max, + "expected an integer between {min} and {max} (got {val})", + ); + Ok(T::try_from(val)?) + } +} +impl Validator for IntegerRangeValidator +where + E: Error + Sync + Send + 'static, + T: Any + TryFrom, +{ + fn validate(&self, val: Value) -> Result { + Ok(ConfigData::new(self.validate(val))) + } +} + +macro_rules! integer_tys { + ($($ty: ident),*) => { + $( + impl Ty for $ty { + fn to_value(&self) -> Value { + Value::Int((*self).try_into().unwrap()) + } + + fn from_value(val: Value) -> Result { + IntegerRangeValidator::new($ty::MIN, $ty::MAX).validate(val) + } + } + )* + + }; +} + +integer_tys! { + i8, i16, i32, isize, + u8, u16, u32 +} + +impl Ty for usize { + fn to_value(&self) -> Value { + Value::Int((*self).try_into().unwrap()) + } + + fn from_value(val: Value) -> Result { + IntegerRangeValidator::new(0usize, isize::MAX as usize).validate(val) + } +} + +impl Ty for u64 { + fn to_value(&self) -> Value { + Value::Int((*self).try_into().unwrap()) + } + + fn from_value(val: Value) -> Result { + IntegerRangeValidator::new(0u64, isize::MAX as u64).validate(val) + } +} + +impl Ty for bool { + fn to_value(&self) -> Value { + Value::Bool(*self) + } + fn from_value(val: Value) -> Result { + let Value::Bool(val) = val else { + bail!("expected a boolean") + }; + Ok(val) + } +} + +impl Ty for Box { + fn to_value(&self) -> Value { + Value::String(self.clone().into_string()) + } + fn from_value(val: Value) -> Result { + let Value::String(val) = val else { + bail!("expected a string") + }; + Ok(val.into_boxed_str()) + } +} + +impl Ty for char { + fn to_value(&self) -> Value { + Value::String(self.to_string()) + } + + fn from_value(val: Value) -> Result { + let Value::String(val) = val else { + bail!("expected a string") + }; + ensure!( + val.chars().count() == 1, + "expecet a single character (got {val:?})" + ); + Ok(val.chars().next().unwrap()) + } +} + +impl Ty for std::string::String { + fn to_value(&self) -> Value { + Value::String(self.clone()) + } + fn from_value(val: Value) -> Result { + let Value::String(val) = val else { + bail!("expected a string") + }; + Ok(val) + } +} + +impl Ty for Option { + fn to_value(&self) -> Value { + match self { + Some(_) => todo!(), + None => todo!(), + } + } + + fn from_value(val: Value) -> Result { + if val == Value::Null { + return Ok(None); + } + Ok(Some(T::from_value(val)?)) + } +} + +impl Ty for Box { + fn from_value(val: Value) -> Result { + Ok(Box::new(T::from_value(val)?)) + } + + fn to_value(&self) -> Value { + T::to_value(self) + } +} + +impl Ty for indexmap::IndexMap, T, ahash::RandomState> { + fn from_value(val: Value) -> Result { + let Value::Map(map) = val else { + bail!("expected a map"); + }; + map.into_iter() + .map(|(k, v)| Ok((k, T::from_value(v)?))) + .collect() + } + + fn to_value(&self) -> Value { + let map = self + .iter() + .map(|(k, v)| (k.clone(), v.to_value())) + .collect(); + Value::Map(Box::new(map)) + } +} + +impl Ty for Box<[T]> { + fn to_value(&self) -> Value { + Value::List(self.iter().map(T::to_value).collect()) + } + fn from_value(val: Value) -> Result { + let Value::List(val) = val else { + bail!("expected a list") + }; + val.iter().cloned().map(T::from_value).collect() + } +} + +impl Ty for serde_json::Value { + fn from_value(val: Value) -> Result { + Ok(val.into()) + } + + fn to_value(&self) -> Value { + self.into() + } +} + +pub(super) struct StaticValidator { + pub(super) ty: PhantomData, +} + +impl Debug for StaticValidator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StaticValidator") + .field("ty", &type_name::()) + .finish() + } +} + +impl Validator for StaticValidator { + fn validate(&self, val: Value) -> Result { + let val = ::from_value(val)?; + Ok(ConfigData::new(val)) + } +} + +pub struct TyValidator { + pub(super) ty: PhantomData, + f: F, +} + +impl Debug for TyValidator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TyValidator") + .field("ty", &type_name::()) + .finish() + } +} + +impl Validator for TyValidator +where + T: Ty, + F: Fn(&T) -> anyhow::Result<()> + 'static, +{ + fn validate(&self, val: Value) -> Result { + let val = ::from_value(val)?; + (self.f)(&val)?; + Ok(ConfigData::new(val)) + } +} + +pub fn ty_validator(f: F) -> impl Validator +where + T: Ty, + F: Fn(&T) -> anyhow::Result<()> + 'static, +{ + TyValidator { ty: PhantomData, f } +} + +pub fn regex_str_validator() -> impl Validator { + ty_validator(|val: &crate::String| { + regex_syntax::parse(val)?; + Ok(()) + }) +} diff --git a/helix-config/src/value.rs b/helix-config/src/value.rs new file mode 100644 index 000000000..be4ce095c --- /dev/null +++ b/helix-config/src/value.rs @@ -0,0 +1,448 @@ +use std::fmt::Display; + +use indexmap::IndexMap; +use serde::de::DeserializeOwned; +use serde::ser::{Error as _, Impossible}; +use serde::{Deserialize, Serialize}; +use serde_json::{Error, Result}; + +use crate::Ty; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Value { + List(Vec), + Map(Box, Value, ahash::RandomState>>), + Int(isize), + Float(f64), + Bool(bool), + String(String), + Null, +} + +impl Value { + pub fn typed(self) -> anyhow::Result { + T::from_value(self) + } + + pub fn append(&mut self, val: Value, depth: usize) { + match (self, val) { + (Value::List(dst), Value::List(ref mut val)) => dst.append(val), + (Value::Map(dst), Value::Map(val)) if depth == 0 || dst.is_empty() => { + dst.extend(val.into_iter()) + } + (Value::Map(dst), Value::Map(val)) => { + dst.reserve(val.len()); + for (k, v) in val.into_iter() { + // we don't use the entry api because we want + // to maintain thhe ordering + let merged = match dst.shift_remove(&k) { + Some(mut old) => { + old.append(v, depth - 1); + old + } + None => v, + }; + dst.insert(k, merged); + } + } + (dst, val) => *dst = val, + } + } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { + Value::String(value.to_owned()) + } +} + +macro_rules! from_int { + ($($ty: ident),*) => { + $( + impl From<$ty> for Value { + fn from(value: $ty) -> Self { + Value::Int(value.try_into().unwrap()) + } + } + )* + }; +} + +impl From for Value { + fn from(value: serde_json::Value) -> Self { + to_value(value).unwrap() + } +} +impl From<&serde_json::Value> for Value { + fn from(value: &serde_json::Value) -> Self { + to_value(value).unwrap() + } +} + +impl From for serde_json::Value { + fn from(value: Value) -> Self { + serde_json::to_value(value).unwrap() + } +} + +from_int!(isize, usize, u32, i32, i16, u16, i8, u8); + +pub fn to_value(value: T) -> Result +where + T: Serialize, +{ + value.serialize(Serializer) +} + +pub fn from_value(value: Value) -> Result +where + T: DeserializeOwned, +{ + // roundtripping trough json is very inefficient *and incorrect* (captures + // json semantics that don't apply to us) + // TODO: implement a custom deserializer just like serde_json does + serde_json::from_value(value.into()) +} + +// We only use our own error type; no need for From conversions provided by the +// standard library's try! macro. This reduces lines of LLVM IR by 4%. +macro_rules! tri { + ($e:expr $(,)?) => { + match $e { + core::result::Result::Ok(val) => val, + core::result::Result::Err(err) => return core::result::Result::Err(err), + } + }; +} + +/// Serializer whose output is a `Value`. +/// +/// This is the serializer that backs [`serde_json::to_value`][crate::to_value]. +/// Unlike the main serde_json serializer which goes from some serializable +/// value of type `T` to JSON text, this one goes from `T` to +/// `serde_json::Value`. +/// +/// The `to_value` function is implementable as: +/// +/// ``` +/// use serde::Serialize; +/// use serde_json::{Error, Value}; +/// +/// pub fn to_value(input: T) -> Result +/// where +/// T: Serialize, +/// { +/// input.serialize(serde_json::value::Serializer) +/// } +/// ``` +pub struct Serializer; + +impl serde::Serializer for Serializer { + type Ok = Value; + type Error = Error; + + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = Impossible; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = Impossible; + + #[inline] + fn serialize_bool(self, value: bool) -> Result { + Ok(Value::Bool(value)) + } + + #[inline] + fn serialize_i8(self, value: i8) -> Result { + self.serialize_i64(value as i64) + } + + #[inline] + fn serialize_i16(self, value: i16) -> Result { + self.serialize_i64(value as i64) + } + + #[inline] + fn serialize_i32(self, value: i32) -> Result { + self.serialize_i64(value as i64) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(Value::Int(value.try_into().unwrap())) + } + + fn serialize_i128(self, _value: i128) -> Result { + unreachable!() + } + + #[inline] + fn serialize_u8(self, value: u8) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u16(self, value: u16) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u32(self, value: u32) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u64(self, value: u64) -> Result { + Ok(Value::Int(value.try_into().unwrap())) + } + + fn serialize_u128(self, _value: u128) -> Result { + unreachable!() + } + + #[inline] + fn serialize_f32(self, float: f32) -> Result { + Ok(Value::Float(float as f64)) + } + + #[inline] + fn serialize_f64(self, float: f64) -> Result { + Ok(Value::Float(float)) + } + + #[inline] + fn serialize_char(self, value: char) -> Result { + let mut s = String::new(); + s.push(value); + Ok(Value::String(s)) + } + + #[inline] + fn serialize_str(self, value: &str) -> Result { + Ok(Value::String(value.into())) + } + + fn serialize_bytes(self, value: &[u8]) -> Result { + let vec = value.iter().map(|&b| Value::Int(b.into())).collect(); + Ok(Value::List(vec)) + } + + #[inline] + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + + #[inline] + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!() + } + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + #[inline] + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + #[inline] + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(SerializeVec { + vec: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!() + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(SerializeMap { + map: IndexMap::default(), + next_key: None, + }) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + unreachable!() + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + Ok(Value::String(value.to_string())) + } +} + +pub struct SerializeVec { + vec: Vec, +} + +impl serde::ser::SerializeSeq for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.vec.push(tri!(to_value(value))); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::List(self.vec)) + } +} + +impl serde::ser::SerializeTuple for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleStruct for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +pub struct SerializeMap { + map: IndexMap, Value, ahash::RandomState>, + next_key: Option>, +} + +impl serde::ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let key = to_value(key)?; + let Value::String(val) = key else { + return Err(Error::custom("only string keys are supported")); + }; + self.next_key = Some(val.into_boxed_str()); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an + // expected failure. + let key = key.expect("serialize_value called before serialize_key"); + self.map.insert(key, tri!(to_value(value))); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Map(Box::new(self.map))) + } +} + +impl serde::ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } +} diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index d7fff6c6f..918359c13 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -17,6 +17,7 @@ integration = [] [dependencies] helix-loader = { path = "../helix-loader" } +helix-config = { path = "../helix-config" } ropey = { version = "1.6.1", default-features = false, features = ["simd"] } smallvec = "1.11" @@ -51,6 +52,8 @@ textwrap = "0.16.0" nucleo.workspace = true parking_lot = "0.12" +anyhow = "1.0.79" +indexmap = { version = "2.1.0", features = ["serde"] } [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs deleted file mode 100644 index 2076fc224..000000000 --- a/helix-core/src/config.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Syntax configuration loader based on built-in languages.toml. -pub fn default_syntax_loader() -> crate::syntax::Configuration { - helix_loader::config::default_lang_config() - .try_into() - .expect("Could not serialize built-in languages.toml") -} -/// Syntax configuration loader based on user configured languages.toml. -pub fn user_syntax_loader() -> Result { - helix_loader::config::user_lang_config()?.try_into() -} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 0acdb2380..b93ee8008 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -3,7 +3,6 @@ pub use encoding_rs as encoding; pub mod auto_pairs; pub mod chars; pub mod comment; -pub mod config; pub mod diagnostic; pub mod diff; pub mod doc_formatter;