Unnamed repository; edit this file 'description' to name the repository.
config system prototype
Pascal Kuthe 2024-01-16
parent 17dd102 · commit 5e74d3c
-rw-r--r--Cargo.lock15
-rw-r--r--Cargo.toml1
-rw-r--r--helix-config/Cargo.toml24
-rw-r--r--helix-config/src/any.rs76
-rw-r--r--helix-config/src/convert.rs42
-rw-r--r--helix-config/src/lib.rs242
-rw-r--r--helix-config/src/macros.rs130
-rw-r--r--helix-config/src/tests.rs80
-rw-r--r--helix-config/src/validator.rs296
-rw-r--r--helix-config/src/value.rs448
-rw-r--r--helix-core/Cargo.toml3
-rw-r--r--helix-core/src/config.rs10
-rw-r--r--helix-core/src/lib.rs1
13 files changed, 1357 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a7e06b6e..e97daba3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1048,6 +1048,19 @@ dependencies = [
]
[[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"
dependencies = [
@@ -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 6c006fbb..77092ba7 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 00000000..ba9bbb5d
--- /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 00000000..891d9e8c
--- /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<T>() -> bool {
+ size_of::<T>() <= size_of::<[usize; 2]>() && align_of::<T>() <= align_of::<[usize; 2]>()
+}
+
+impl ConfigData {
+ unsafe fn drop_impl<T: Any>(mut data: MaybeUninit<[usize; 2]>) {
+ if store_inline::<T>() {
+ data.as_mut_ptr().cast::<T>().drop_in_place();
+ } else {
+ let ptr = data.as_mut_ptr().cast::<*mut T>().read();
+ drop(Box::from_raw(ptr));
+ }
+ }
+
+ pub fn get<T: Any>(&self) -> &T {
+ assert_eq!(TypeId::of::<T>(), self.ty);
+ unsafe {
+ if store_inline::<T>() {
+ return &*self.data.as_ptr().cast();
+ }
+ let data: *const T = self.data.as_ptr().cast::<*const T>().read();
+ &*data
+ }
+ }
+ pub fn new<T: Any>(val: T) -> Self {
+ let mut data = MaybeUninit::uninit();
+ if store_inline::<T>() {
+ 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::<T>(),
+ drop_fn: ConfigData::drop_impl::<T>,
+ }
+ }
+}
+
+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 00000000..1ee3b6f7
--- /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<T: Ty> IntoTy for T {
+ type Ty = Self;
+
+ fn into_ty(self) -> Self::Ty {
+ self
+ }
+}
+impl<T: IntoTy> IntoTy for &[T] {
+ type Ty = Box<[T::Ty]>;
+
+ fn into_ty(self) -> Self::Ty {
+ self.iter().cloned().map(T::into_ty).collect()
+ }
+}
+impl<T: IntoTy, const N: usize> 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<str>;
+
+ fn into_ty(self) -> Self::Ty {
+ self.into()
+ }
+}
+
+pub(super) fn ty_into_value<T: Ty>(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 00000000..8f27e41e
--- /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<T> = IndexMap<Box<str>, T, ahash::RandomState>;
+pub type String = Box<str>;
+pub type List<T> = Box<[T]>;
+
+#[cfg(test)]
+mod tests;
+
+#[derive(Debug)]
+pub struct OptionInfo {
+ pub name: Arc<str>,
+ pub description: Box<str>,
+ pub validator: Box<dyn Validator>,
+ pub into_value: fn(&ConfigData) -> Value,
+}
+
+#[derive(Debug)]
+pub struct OptionManager {
+ vals: RwLock<HashMap<Arc<str>, ConfigData>>,
+ parent: Option<Arc<OptionManager>>,
+}
+
+impl OptionManager {
+ pub fn get<T: Any>(&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<T: Deref + Any>(&self, option: &str) -> Guard<'_, T::Target> {
+ Guard::map(self.get::<T>(option), T::deref)
+ }
+
+ pub fn get_folded<T: Any, R>(
+ &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<Arc<str>>,
+ registry: &OptionRegistry,
+ ) -> anyhow::Result<Value> {
+ let option: Arc<str> = 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 {
+ OptionManager {
+ vals: RwLock::default(),
+ parent: Some(self.clone()),
+ }
+ }
+
+ pub fn set_parent_scope(&mut self, parent: Arc<OptionManager>) {
+ self.parent = Some(parent)
+ }
+
+ pub fn set_unchecked(&self, option: Arc<str>, val: ConfigData) {
+ self.vals.write().insert(option, val);
+ }
+
+ pub fn append(
+ &self,
+ option: impl Into<Arc<str>>,
+ val: impl Into<Value>,
+ registry: &OptionRegistry,
+ max_depth: usize,
+ ) -> anyhow::Result<()> {
+ let val = val.into();
+ let option: Arc<str> = 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<Arc<str>>,
+ val: impl Into<Value>,
+ registry: &OptionRegistry,
+ ) -> anyhow::Result<()> {
+ let option: Arc<str> = 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<Arc<str>, OptionInfo>,
+ defaults: Arc<OptionManager>,
+}
+
+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<T: IntoTy>(&mut self, name: &str, description: &str, default: T) {
+ self.register_with_validator(
+ name,
+ description,
+ default,
+ StaticValidator::<T::Ty> { ty: PhantomData },
+ );
+ }
+
+ pub fn register_with_validator<T: IntoTy>(
+ &mut self,
+ name: &str,
+ description: &str,
+ default: T,
+ validator: impl Validator,
+ ) {
+ let mut name: Arc<str> = 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::<T::Ty>,
+ };
+ 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<OptionManager> {
+ 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 00000000..69e9c75c
--- /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<T>` (which allows derferecning to
+/// &T). Any type that implements copy can be returned as a copy instead by
+/// specifying `#[read = copy]`. Collections like `List<T>` 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<T>. 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 = "<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<Self> {
+ 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 00000000..6a724bd6
--- /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::<Box<[_]>>("shell");
+ let shell: &[Box<str>] = &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", &registry).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", &registry).unwrap();
+ let line_number: LineNumber = *scope_3.get("line-number");
+ assert_eq!(line_number, LineNumber::Relative);
+ scope_2.set("line-number", "abs", &registry).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 00000000..7c56c358
--- /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<ConfigData>;
+}
+
+pub trait Ty: Sized + Clone + 'static {
+ fn from_value(val: Value) -> Result<Self>;
+ fn to_value(&self) -> Value;
+}
+
+#[derive(Clone, Copy)]
+pub struct IntegerRangeValidator<T> {
+ pub min: isize,
+ pub max: isize,
+ ty: PhantomData<T>,
+}
+impl<E, T> IntegerRangeValidator<T>
+where
+ E: Debug,
+ T: TryInto<isize, Error = E>,
+{
+ pub fn new(min: T, max: T) -> Self {
+ Self {
+ min: min.try_into().unwrap(),
+ max: max.try_into().unwrap(),
+ ty: PhantomData,
+ }
+ }
+}
+
+impl<T> Debug for IntegerRangeValidator<T> {
+ 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::<T>())
+ .finish()
+ }
+}
+
+impl<E, T> IntegerRangeValidator<T>
+where
+ E: Error + Sync + Send + 'static,
+ T: Any + TryFrom<isize, Error = E>,
+{
+ pub fn validate(&self, val: Value) -> Result<T> {
+ 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<E, T> Validator for IntegerRangeValidator<T>
+where
+ E: Error + Sync + Send + 'static,
+ T: Any + TryFrom<isize, Error = E>,
+{
+ fn validate(&self, val: Value) -> Result<ConfigData> {
+ 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ let Value::Bool(val) = val else {
+ bail!("expected a boolean")
+ };
+ Ok(val)
+ }
+}
+
+impl Ty for Box<str> {
+ fn to_value(&self) -> Value {
+ Value::String(self.clone().into_string())
+ }
+ fn from_value(val: Value) -> Result<Self> {
+ 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<Self> {
+ 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<Self> {
+ let Value::String(val) = val else {
+ bail!("expected a string")
+ };
+ Ok(val)
+ }
+}
+
+impl<T: Ty> Ty for Option<T> {
+ fn to_value(&self) -> Value {
+ match self {
+ Some(_) => todo!(),
+ None => todo!(),
+ }
+ }
+
+ fn from_value(val: Value) -> Result<Self> {
+ if val == Value::Null {
+ return Ok(None);
+ }
+ Ok(Some(T::from_value(val)?))
+ }
+}
+
+impl<T: Ty> Ty for Box<T> {
+ fn from_value(val: Value) -> Result<Self> {
+ Ok(Box::new(T::from_value(val)?))
+ }
+
+ fn to_value(&self) -> Value {
+ T::to_value(self)
+ }
+}
+
+impl<T: Ty> Ty for indexmap::IndexMap<Box<str>, T, ahash::RandomState> {
+ fn from_value(val: Value) -> Result<Self> {
+ 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<T: Ty> Ty for Box<[T]> {
+ fn to_value(&self) -> Value {
+ Value::List(self.iter().map(T::to_value).collect())
+ }
+ fn from_value(val: Value) -> Result<Self> {
+ 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<Self> {
+ Ok(val.into())
+ }
+
+ fn to_value(&self) -> Value {
+ self.into()
+ }
+}
+
+pub(super) struct StaticValidator<T: Ty> {
+ pub(super) ty: PhantomData<fn(&T)>,
+}
+
+impl<T: Ty> Debug for StaticValidator<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("StaticValidator")
+ .field("ty", &type_name::<T>())
+ .finish()
+ }
+}
+
+impl<T: Ty> Validator for StaticValidator<T> {
+ fn validate(&self, val: Value) -> Result<ConfigData> {
+ let val = <T as Ty>::from_value(val)?;
+ Ok(ConfigData::new(val))
+ }
+}
+
+pub struct TyValidator<F, T: Ty> {
+ pub(super) ty: PhantomData<fn(&T)>,
+ f: F,
+}
+
+impl<T: Ty, F> Debug for TyValidator<F, T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TyValidator")
+ .field("ty", &type_name::<T>())
+ .finish()
+ }
+}
+
+impl<T, F> Validator for TyValidator<F, T>
+where
+ T: Ty,
+ F: Fn(&T) -> anyhow::Result<()> + 'static,
+{
+ fn validate(&self, val: Value) -> Result<ConfigData> {
+ let val = <T as Ty>::from_value(val)?;
+ (self.f)(&val)?;
+ Ok(ConfigData::new(val))
+ }
+}
+
+pub fn ty_validator<T, F>(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 00000000..be4ce095
--- /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<Value>),
+ Map(Box<IndexMap<Box<str>, Value, ahash::RandomState>>),
+ Int(isize),
+ Float(f64),
+ Bool(bool),
+ String(String),
+ Null,
+}
+
+impl Value {
+ pub fn typed<T: Ty>(self) -> anyhow::Result<T> {
+ 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<serde_json::Value> 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<Value> 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<T>(value: T) -> Result<Value>
+where
+ T: Serialize,
+{
+ value.serialize(Serializer)
+}
+
+pub fn from_value<T>(value: Value) -> Result<T>
+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<T>(input: T) -> Result<Value, Error>
+/// 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<Value, Error>;
+ type SerializeMap = SerializeMap;
+ type SerializeStruct = SerializeMap;
+ type SerializeStructVariant = Impossible<Value, Error>;
+
+ #[inline]
+ fn serialize_bool(self, value: bool) -> Result<Value> {
+ Ok(Value::Bool(value))
+ }
+
+ #[inline]
+ fn serialize_i8(self, value: i8) -> Result<Value> {
+ self.serialize_i64(value as i64)
+ }
+
+ #[inline]
+ fn serialize_i16(self, value: i16) -> Result<Value> {
+ self.serialize_i64(value as i64)
+ }
+
+ #[inline]
+ fn serialize_i32(self, value: i32) -> Result<Value> {
+ self.serialize_i64(value as i64)
+ }
+
+ fn serialize_i64(self, value: i64) -> Result<Value> {
+ Ok(Value::Int(value.try_into().unwrap()))
+ }
+
+ fn serialize_i128(self, _value: i128) -> Result<Value> {
+ unreachable!()
+ }
+
+ #[inline]
+ fn serialize_u8(self, value: u8) -> Result<Value> {
+ self.serialize_u64(value as u64)
+ }
+
+ #[inline]
+ fn serialize_u16(self, value: u16) -> Result<Value> {
+ self.serialize_u64(value as u64)
+ }
+
+ #[inline]
+ fn serialize_u32(self, value: u32) -> Result<Value> {
+ self.serialize_u64(value as u64)
+ }
+
+ #[inline]
+ fn serialize_u64(self, value: u64) -> Result<Value> {
+ Ok(Value::Int(value.try_into().unwrap()))
+ }
+
+ fn serialize_u128(self, _value: u128) -> Result<Value> {
+ unreachable!()
+ }
+
+ #[inline]
+ fn serialize_f32(self, float: f32) -> Result<Value> {
+ Ok(Value::Float(float as f64))
+ }
+
+ #[inline]
+ fn serialize_f64(self, float: f64) -> Result<Value> {
+ Ok(Value::Float(float))
+ }
+
+ #[inline]
+ fn serialize_char(self, value: char) -> Result<Value> {
+ let mut s = String::new();
+ s.push(value);
+ Ok(Value::String(s))
+ }
+
+ #[inline]
+ fn serialize_str(self, value: &str) -> Result<Value> {
+ Ok(Value::String(value.into()))
+ }
+
+ fn serialize_bytes(self, value: &[u8]) -> Result<Value> {
+ let vec = value.iter().map(|&b| Value::Int(b.into())).collect();
+ Ok(Value::List(vec))
+ }
+
+ #[inline]
+ fn serialize_unit(self) -> Result<Value> {
+ Ok(Value::Null)
+ }
+
+ #[inline]
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
+ unimplemented!()
+ }
+
+ #[inline]
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Value> {
+ self.serialize_str(variant)
+ }
+
+ #[inline]
+ fn serialize_newtype_struct<T>(self, _name: &'static str, _value: &T) -> Result<Value>
+ where
+ T: ?Sized + Serialize,
+ {
+ unimplemented!()
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<Value>
+ where
+ T: ?Sized + Serialize,
+ {
+ unimplemented!()
+ }
+
+ #[inline]
+ fn serialize_none(self) -> Result<Value> {
+ self.serialize_unit()
+ }
+
+ #[inline]
+ fn serialize_some<T>(self, value: &T) -> Result<Value>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Ok(SerializeVec {
+ vec: Vec::with_capacity(len.unwrap_or(0)),
+ })
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ unimplemented!()
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Ok(SerializeMap {
+ map: IndexMap::default(),
+ next_key: None,
+ })
+ }
+
+ fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+ unreachable!()
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ unreachable!()
+ }
+
+ fn collect_str<T>(self, value: &T) -> Result<Value>
+ where
+ T: ?Sized + Display,
+ {
+ Ok(Value::String(value.to_string()))
+ }
+}
+
+pub struct SerializeVec {
+ vec: Vec<Value>,
+}
+
+impl serde::ser::SerializeSeq for SerializeVec {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.vec.push(tri!(to_value(value)));
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::List(self.vec))
+ }
+}
+
+impl serde::ser::SerializeTuple for SerializeVec {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Value> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+impl serde::ser::SerializeTupleStruct for SerializeVec {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Value> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+pub struct SerializeMap {
+ map: IndexMap<Box<str>, Value, ahash::RandomState>,
+ next_key: Option<Box<str>>,
+}
+
+impl serde::ser::SerializeMap for SerializeMap {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_key<T>(&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<T>(&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<Value> {
+ Ok(Value::Map(Box::new(self.map)))
+ }
+}
+
+impl serde::ser::SerializeStruct for SerializeMap {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeMap::serialize_entry(self, key, value)
+ }
+
+ fn end(self) -> Result<Value> {
+ serde::ser::SerializeMap::end(self)
+ }
+}
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index d7fff6c6..918359c1 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 2076fc22..00000000
--- 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<crate::syntax::Configuration, toml::de::Error> {
- helix_loader::config::user_lang_config()?.try_into()
-}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 0acdb238..b93ee800 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;