Created
November 28, 2023 13:21
-
-
Save A-Walrus/46ba820f59a562105c33010bb0c9ff33 to your computer and use it in GitHub Desktop.
POC for Helix configuration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use config::{OptionManager, Value, OPTIONS_REGISTRY}; | |
fn main() -> anyhow::Result<()> { | |
let mut manager = OptionManager::new_with_defaults(); | |
// Get documentation for an option: | |
let desc = OPTIONS_REGISTRY.get("line-number").unwrap().description; | |
dbg!(desc); | |
// Mapping to and from `serde_json` is optional and could easily be replaced | |
// by scheme... | |
// Set values for example from `:set` or when parsing config file: | |
manager.set("mouse", Value::Bool(true)).unwrap(); | |
manager | |
.set("shell", serde_json::from_str("[\"cmd\", \"-C\"]").unwrap()) | |
.unwrap(); | |
// Get and print value of an option as a `Value` (`:get-option`) | |
println!( | |
"{}", | |
serde_json::to_string(manager.get_value("line-number")?)? | |
); | |
// Get *typed* values of config options to use in code. | |
let scrolloff: &isize = manager.get("scrolloff").unwrap(); | |
let shell: &Vec<String> = manager.get("shell").unwrap(); | |
dbg!(scrolloff); | |
dbg!(shell); | |
// Like generating docs | |
for (_, info) in OPTIONS_REGISTRY.0.iter() { | |
let _name = info.name; | |
let _desc = info.description; | |
let _default = serde_json::to_string(&info.default); | |
} | |
Ok(()) | |
} | |
/// Config module | |
mod config { | |
mod types { | |
/// Helper Types ----------- | |
pub enum GutterType { | |
/// Show diagnostics and other features like breakpoints | |
Diagnostics, | |
/// Show line numbers | |
LineNumbers, | |
/// Show one blank space | |
Spacer, | |
/// Highlight local changes | |
Diff, | |
} | |
pub enum LineNumber { | |
Absolute, | |
Relative, | |
} | |
pub enum BufferLine { | |
Always, | |
Never, | |
Multiple, | |
} | |
pub enum LineEnding { | |
Native, | |
Lf, | |
CrLf, | |
Ff, | |
Cr, | |
Nel, | |
} | |
} | |
use anyhow::anyhow; | |
use lazy_static::lazy_static; | |
use types::*; | |
use std::{ | |
any::{type_name, Any, TypeId}, | |
collections::HashMap, | |
time::Duration, | |
}; | |
use serde::{Deserialize, Serialize}; | |
lazy_static! { | |
pub static ref OPTIONS_REGISTRY: OptionRegistry = OptionRegistry::default(); | |
} | |
pub struct OptionRegistry(pub HashMap<String, OptionInfo>); | |
pub struct OptionManager { | |
options: HashMap<String, (Value, Box<dyn Any>)>, | |
} | |
impl Default for OptionRegistry { | |
fn default() -> Self { | |
OptionRegistry::new([ | |
OptionInfo::new_with_tryfrom::<isize>( | |
"scrolloff", | |
"Number of lines of padding around the edge of the screen when scrolling", | |
Value::Int(5), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"mouse", | |
"Enable mouse mode", | |
Value::Bool(true), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"middle-click-paste", | |
"Middle click paste support", | |
Value::Bool(true), | |
), | |
OptionInfo::new_with_tryfrom::<usize>( | |
"scroll-lines", | |
"Number of lines to scroll per scroll wheel step", | |
Value::Int(3), | |
), | |
OptionInfo::new_with_tryfrom::<Vec<String>>( | |
"shell", | |
"Shell to use when running external commands", | |
Value::List(vec!["sh".into(), "-c".into()]), | |
), | |
OptionInfo::new::<LineNumber>( | |
"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", | |
Value::String("absolute".to_owned()), | |
|v| { | |
let s: String = v.try_into()?; | |
match s.as_str() { | |
"absolute" | "abs" => Ok(LineNumber::Absolute), | |
"relative" | "rel" => Ok(LineNumber::Relative), | |
_ => Err(anyhow!( | |
"line-number must be either `absolute` or `relative`" | |
)), | |
} | |
}, | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"cursorline", | |
"Highlight all lines with a cursor", | |
Value::Bool(false), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"cursorcolumn", | |
"Highlight all columns with a cursor", | |
Value::Bool(false), | |
), | |
OptionInfo::new::<Vec<GutterType>>( | |
"gutters", | |
"Gutters to display: Available are `diagnostics` and `diff` and \ | |
`line-numbers` and `spacer`, note that `diagnostics` also includes other \ | |
features like breakpoints, 1-width padding will be inserted if gutters is \ | |
non-empty", | |
Value::List(vec![ | |
"diagnostics".into(), | |
"spacer".into(), | |
"line-numbers".into(), | |
"spacer".into(), | |
"diff".into(), | |
]), | |
|v| { | |
let v: Vec<String> = v.try_into()?; | |
v.into_iter() | |
.map(|s| { | |
Ok(match s.as_str() { | |
"diagnostics" => GutterType::Diagnostics, | |
"line-numbers" => GutterType::LineNumbers, | |
"diff" => GutterType::Diff, | |
"spacer" => GutterType::Spacer, | |
_ => anyhow::bail!( | |
"Items in gutter must be `diagnostics`, `line-numbers`, \ | |
`diff` or `spacer`" | |
), | |
}) | |
}) | |
.collect() | |
}, | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"auto-completion", | |
"Enabe automatic pop up of auto-completion", | |
Value::Bool(true), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"auto-format", | |
"Enable automatic formatting on save", | |
Value::Bool(true), | |
), | |
OptionInfo::new::<Duration>( | |
"idle-timeout", | |
"Time in milliseconds since last keypress before idle timers trigger. Used \ | |
for autompletion, set to 0 for instant", | |
Value::Int(400), | |
|v| { | |
let millis: usize = v.try_into()?; | |
Ok(Duration::from_millis(millis as u64)) | |
}, | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"preview-completion-insert", | |
"Whether to apply commpletion item instantly when selected", | |
Value::Bool(true), | |
), | |
OptionInfo::new_with_tryfrom::<usize>( | |
"completion-trigger-len", | |
"The min-length of word under cursor to trigger autocompletion", | |
Value::Int(2), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"completion-replace", | |
"Set to `true` to make completions always replace the entire word and not \ | |
just the part before the cursor", | |
Value::Bool(false), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"auto-info", | |
"Whether to display info boxes", | |
Value::Bool(true), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"true-color", | |
"Set to `true` to override automatic detection of terminal truecolor support \ | |
in the event of a false negative", | |
Value::Bool(false), | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"undercurl", | |
"Set to `true` to override automatic detection of terminal undercurl support \ | |
in the event of a false negative", | |
Value::Bool(false), | |
), | |
OptionInfo::new_with_tryfrom::<Vec<usize>>( | |
"rulers", | |
"List of column positions at which to display the rulers. Can be overridden \ | |
by language specific `rulers` in `languages.toml` file", | |
Value::List(Vec::new()), | |
), | |
OptionInfo::new::<BufferLine>( | |
"bufferline", | |
"Renders a line at the top of the editor displaying open buffers. Can be \ | |
`always`, `never` or `multiple` (only shown if more than one buffer is in \ | |
use)", | |
Value::String("never".to_owned()), | |
|v| { | |
let s: String = v.try_into()?; | |
match s.as_str() { | |
"never" => Ok(BufferLine::Never), | |
"always" => Ok(BufferLine::Always), | |
"multiple" => Ok(BufferLine::Multiple), | |
_ => Err(anyhow!( | |
"bufferline must be either `never`, `always`, or `multiple`" | |
)), | |
} | |
}, | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"color-modes", | |
"Whether to color the mode indicator with different colors depending on the \ | |
mode itself", | |
Value::Bool(false), | |
), | |
OptionInfo::new_with_tryfrom::<usize>( | |
"text-width", | |
"Maximum line length. Used for the `:reflow` command and soft-wrapping if \ | |
`soft-wrap.wrap-at-text-width` is set", | |
Value::Int(80), | |
), | |
OptionInfo::new_with_tryfrom::<Vec<String>>( | |
"workspace-lsp-roots", | |
"Directories relative to the workspace root that are treated as LSP roots. \ | |
Should only be set in `.helix/config.toml`", | |
Value::List(Vec::new()), | |
), | |
OptionInfo::new::<LineEnding>( | |
"default-line-ending", | |
"The line ending to use for new documents. Can be `native`, `lf`, `crlf`, \ | |
`ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` \ | |
on Windows, otherwise `lf`).", | |
Value::String("native".to_owned()), | |
|v| { | |
let s: String = v.try_into()?; | |
match s.as_str() { | |
"native" => Ok(LineEnding::Native), | |
"lf" => Ok(LineEnding::Lf), | |
"crLf" => Ok(LineEnding::CrLf), | |
"ff" => Ok(LineEnding::Ff), | |
"cr" => Ok(LineEnding::Cr), | |
"nel" => Ok(LineEnding::Nel), | |
_ => Err(anyhow!( | |
"default-line-ending must be `native`, `lf`, `crLf`, `ff`, `cr`, \ | |
or `nel`" | |
)), | |
} | |
}, | |
), | |
OptionInfo::new_with_tryfrom::<bool>( | |
"insert-final-newline", | |
"Whether to automatically insert a trailing line-ending on write if missing", | |
Value::Bool(true), | |
), | |
]) | |
} | |
} | |
impl OptionRegistry { | |
pub fn new(registry: impl IntoIterator<Item = OptionInfo>) -> Self { | |
let registry: HashMap<String, OptionInfo> = registry | |
.into_iter() | |
.map(|info| (info.name.to_owned(), info)) | |
.collect(); | |
Self(registry) | |
} | |
pub fn get(&self, k: &str) -> anyhow::Result<&OptionInfo> { | |
self.0.get(k).ok_or(anyhow!("Option not in registry")) | |
} | |
} | |
impl OptionManager { | |
pub fn new_with_defaults() -> Self { | |
let registry: &OptionRegistry = &OPTIONS_REGISTRY; | |
let mut this = Self::empty(); | |
let defaults: Vec<(String, Value)> = registry | |
.0 | |
.iter() | |
.map(|(name, info)| (name.clone(), info.default.clone())) | |
.collect(); | |
for (name, default) in defaults { | |
this.set(&name, default) | |
.expect("All default values should convert correctly"); | |
} | |
this | |
} | |
pub fn empty() -> Self { | |
Self { | |
options: HashMap::new(), | |
} | |
} | |
pub fn get<T: Any>(&self, name: &str) -> anyhow::Result<&T> { | |
let info = OPTIONS_REGISTRY.get(name)?; | |
assert_eq!( | |
TypeId::of::<T>(), | |
info.type_id, | |
"Option type `{}` doesn't match requested type `{}`.", | |
info.type_name, | |
type_name::<T>() | |
); | |
let (_, any) = self | |
.options | |
.get(name) | |
.ok_or(anyhow!("Option not in data"))?; // TODO heirarchy | |
Ok(any.downcast_ref::<T>().unwrap()) | |
} | |
pub fn get_value(&self, name: &str) -> anyhow::Result<&Value> { | |
let (value, _) = self | |
.options | |
.get(name) | |
.ok_or(anyhow!("Option not in data"))?; // TODO heirarchy | |
Ok(value) | |
} | |
pub fn set(&mut self, name: &str, value: Value) -> anyhow::Result<()> { | |
let info = OPTIONS_REGISTRY.get(name)?; | |
let converted = (info.converter)(value.clone())?; | |
self.options.insert(name.to_owned(), (value, converted)); | |
Ok(()) | |
} | |
} | |
#[derive(Clone, Serialize, Deserialize, Debug)] | |
#[serde(untagged)] | |
pub enum Value { | |
List(Vec<Value>), | |
Dict(Box<HashMap<String, Value>>), | |
Int(isize), | |
Bool(bool), | |
String(String), | |
} | |
impl TryFrom<Value> for isize { | |
type Error = anyhow::Error; | |
fn try_from(value: Value) -> Result<Self, Self::Error> { | |
if let Value::Int(v) = value { | |
Ok(v) | |
} else { | |
Err(anyhow!("Value is not an Integer")) | |
} | |
} | |
} | |
impl TryFrom<Value> for usize { | |
type Error = anyhow::Error; | |
fn try_from(value: Value) -> Result<Self, Self::Error> { | |
if let Value::Int(v) = value { | |
Ok(v.try_into()?) | |
} else { | |
Err(anyhow!("Value is not an Integer")) | |
} | |
} | |
} | |
impl TryFrom<Value> for bool { | |
type Error = anyhow::Error; | |
fn try_from(value: Value) -> Result<Self, Self::Error> { | |
if let Value::Bool(v) = value { | |
Ok(v) | |
} else { | |
Err(anyhow!("Value is not a Boolean")) | |
} | |
} | |
} | |
impl TryFrom<Value> for String { | |
type Error = anyhow::Error; | |
fn try_from(value: Value) -> Result<Self, Self::Error> { | |
if let Value::String(v) = value { | |
Ok(v) | |
} else { | |
Err(anyhow!("Value is not a String")) | |
} | |
} | |
} | |
impl<T: TryFrom<Value>> TryFrom<Value> for Vec<T> { | |
type Error = anyhow::Error; | |
fn try_from(value: Value) -> Result<Self, Self::Error> { | |
if let Value::List(list) = value { | |
list.into_iter() | |
.map(|x| match x.try_into() { | |
Ok(s) => Ok(s), | |
Err(_) => { | |
anyhow::bail!("Value in list is not of type `{}`", type_name::<T>()) | |
} | |
}) | |
.collect() | |
} else { | |
Err(anyhow!("Value is not of type List")) | |
} | |
} | |
} | |
impl From<&str> for Value { | |
fn from(value: &str) -> Self { | |
Value::String(value.to_owned()) | |
} | |
} | |
impl From<isize> for Value { | |
fn from(value: isize) -> Self { | |
Value::Int(value) | |
} | |
} | |
/// # Invariants: | |
/// converter must always return value of type specified by TypeId | |
pub struct OptionInfo { | |
pub name: &'static str, // TODO Is this necessary | |
pub description: &'static str, | |
pub default: Value, | |
type_id: TypeId, | |
type_name: &'static str, | |
converter: Box<dyn Fn(Value) -> anyhow::Result<Box<dyn Any>> + Sync>, | |
} | |
impl OptionInfo { | |
pub fn new<T: Any>( | |
name: &'static str, | |
description: &'static str, | |
default: Value, | |
converter: fn(Value) -> anyhow::Result<T>, | |
) -> Self { | |
Self { | |
name, | |
description, | |
default, | |
type_id: TypeId::of::<T>(), | |
type_name: type_name::<T>(), | |
converter: Box::new(move |value| Ok(Box::new(converter(value)?))), | |
} | |
} | |
pub fn new_with_tryfrom<T: Any + TryFrom<Value>>( | |
name: &'static str, | |
description: &'static str, | |
default: Value, | |
) -> Self { | |
Self { | |
name, | |
description, | |
default, | |
type_id: TypeId::of::<T>(), | |
type_name: type_name::<T>(), | |
converter: Box::new(move |value| { | |
Ok(Box::new( | |
TryInto::<T>::try_into(value) | |
.map_err(|_| anyhow!("Value is not the correct type"))?, | |
)) | |
}), | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment