config system prototype

This commit is contained in:
Pascal Kuthe 2024-01-11 17:54:57 +01:00
parent 17dd102e5c
commit 5e74d3c821
No known key found for this signature in database
GPG Key ID: D715E8655AE166A6
13 changed files with 1357 additions and 11 deletions

15
Cargo.lock generated
View File

@ -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",

View File

@ -2,6 +2,7 @@
resolver = "2"
members = [
"helix-core",
"helix-config",
"helix-view",
"helix-term",
"helix-tui",

24
helix-config/Cargo.toml Normal file
View File

@ -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"

76
helix-config/src/any.rs Normal file
View File

@ -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 {}

View File

@ -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())
}

242
helix-config/src/lib.rs Normal file
View File

@ -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()
}
}

130
helix-config/src/macros.rs Normal file
View File

@ -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)
}
}
};
}

80
helix-config/src/tests.rs Normal file
View File

@ -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);
}

View File

@ -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(())
})
}

448
helix-config/src/value.rs Normal file
View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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()
}

View File

@ -3,7 +3,6 @@
pub mod auto_pairs;
pub mod chars;
pub mod comment;
pub mod config;
pub mod diagnostic;
pub mod diff;
pub mod doc_formatter;