Skip to content

Instantly share code, notes, and snippets.

@A-Walrus
Created November 28, 2023 13:21
Show Gist options
  • Save A-Walrus/46ba820f59a562105c33010bb0c9ff33 to your computer and use it in GitHub Desktop.
Save A-Walrus/46ba820f59a562105c33010bb0c9ff33 to your computer and use it in GitHub Desktop.
POC for Helix configuration
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