From 2e03ce52e9dbad3ae211ab8060be6e8a96b6514a Mon Sep 17 00:00:00 2001 From: Rolo Date: Thu, 26 Dec 2024 00:06:51 -0800 Subject: [PATCH] feat: add `[commands]` table to config parsing --- helix-term/src/config.rs | 131 +++++++++++++++++++++++++++--- helix-view/src/commands/custom.rs | 47 ++--------- 2 files changed, 127 insertions(+), 51 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index bcba8d8e1d45..7ebf37e70388 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,12 +1,14 @@ use crate::keymap; use crate::keymap::{merge_keys, KeyTrie}; use helix_loader::merge_toml_values; +use helix_view::commands::custom::CustomTypableCommand; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; use std::fmt::Display; use std::fs; use std::io::Error as IOError; +use std::sync::Arc; use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq)] @@ -22,6 +24,7 @@ pub struct ConfigRaw { pub theme: Option, pub keys: Option>, pub editor: Option, + commands: Option, } impl Default for Config { @@ -65,7 +68,7 @@ impl Config { let local_config: Result = local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); let res = match (global_config, local_config) { - (Ok(global), Ok(local)) => { + (Ok(mut global), Ok(local)) => { let mut keys = keymap::default(); if let Some(global_keys) = global.keys { merge_keys(&mut keys, global_keys) @@ -74,7 +77,7 @@ impl Config { merge_keys(&mut keys, local_keys) } - let editor = match (global.editor, local.editor) { + let mut editor = match (global.editor, local.editor) { (None, None) => helix_view::editor::Config::default(), (None, Some(val)) | (Some(val), None) => { val.try_into().map_err(ConfigLoadError::BadConfig)? @@ -84,6 +87,26 @@ impl Config { .map_err(ConfigLoadError::BadConfig)?, }; + // Merge locally defined commands, overwriting global space commands if encountered + if let Some(lcommands) = local.commands { + if let Some(gcommands) = &mut global.commands { + for (name, details) in lcommands.commands { + gcommands.commands.insert(name, details); + } + } else { + global.commands = Some(lcommands); + } + } + + // If any commands were defined anywhere, add to editor + if let Some(commands) = global.commands { + editor.commands.commands = commands + .commands + .into_iter() + .map(|(name, details)| details.into_custom_command(name)) + .collect(); + } + Config { theme: local.theme.or(global.theme), keys, @@ -100,13 +123,25 @@ impl Config { if let Some(keymap) = config.keys { merge_keys(&mut keys, keymap); } + + let mut editor = config.editor.map_or_else( + || Ok(helix_view::editor::Config::default()), + |val| val.try_into().map_err(ConfigLoadError::BadConfig), + )?; + + // Add custom commands + if let Some(commands) = config.commands { + editor.commands.commands = commands + .commands + .into_iter() + .map(|(name, details)| details.into_custom_command(name)) + .collect(); + } + Config { theme: config.theme, keys, - editor: config.editor.map_or_else( - || Ok(helix_view::editor::Config::default()), - |val| val.try_into().map_err(ConfigLoadError::BadConfig), - )?, + editor, } } @@ -126,13 +161,75 @@ impl Config { } } +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +struct Commands { + #[serde(flatten)] + commands: HashMap, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[serde(untagged)] +enum CommandTomlType { + Single(String), + Multiple(Vec), + Detailed { + commands: Vec, + desc: Option, + accepts: Option, + completer: Option, + }, +} + +impl CommandTomlType { + fn into_custom_command(self, name: String) -> CustomTypableCommand { + let name = name.trim_start_matches(':'); + match self { + Self::Single(command) => CustomTypableCommand { + name: Arc::from(name), + desc: None, + commands: vec![Arc::from(command.trim_start_matches(':'))].into(), + accepts: None, + completer: None, + }, + Self::Multiple(commands) => CustomTypableCommand { + name: Arc::from(name), + desc: None, + commands: commands + .into_iter() + .map(|command| Arc::from(command.trim_start_matches(':'))) + .collect::>() + .into(), + accepts: None, + completer: None, + }, + Self::Detailed { + commands, + desc, + accepts, + completer, + } => CustomTypableCommand { + name: Arc::from(name), + desc: desc.map(Arc::from), + commands: commands + .into_iter() + .map(|command| Arc::from(command.trim_start_matches(':'))) + .collect::>() + .into(), + accepts: accepts.map(|accepts| Arc::from(accepts.trim_start_matches(':'))), + completer: completer.map(|completer| Arc::from(completer.trim_start_matches(':'))), + }, + } + } +} + #[cfg(test)] mod tests { + use super::*; impl Config { - fn load_test(config: &str) -> Config { - Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + fn load_test(config: &str) -> Result { + Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())) } } @@ -166,7 +263,7 @@ mod tests { ); assert_eq!( - Config::load_test(sample_keymaps), + Config::load_test(sample_keymaps).unwrap(), Config { keys, ..Default::default() @@ -177,11 +274,25 @@ mod tests { #[test] fn keys_resolve_to_correct_defaults() { // From serde default - let default_keys = Config::load_test("").keys; + let default_keys = Config::load_test("").unwrap().keys; assert_eq!(default_keys, keymap::default()); // From the Default trait let default_keys = Config::default().keys; assert_eq!(default_keys, keymap::default()); } + + #[test] + fn should_deserialize_commands() { + let config = r#" +[commands] +":wq" = [":write", "quit"] +":w" = ":write --force" +":wcd!" = { commands = [':write --force %{arg}', ':cd %sh{ %{arg} | path dirname }'], desc = "writes buffer to disk forcefully, then changes to its directory", accepts = "", completer = ":write" } +"#; + + if let Err(err) = Config::load_test(config) { + panic!("{err:#?}") + }; + } } diff --git a/helix-view/src/commands/custom.rs b/helix-view/src/commands/custom.rs index 5cfa4f1e743e..954126264a5e 100644 --- a/helix-view/src/commands/custom.rs +++ b/helix-view/src/commands/custom.rs @@ -1,9 +1,3 @@ -// TODO: When adding custom aliases to the command prompt list, must priotize the custom over the built-in. -// - Should include removing the alias from the aliases command? -// -// TODO: Need to get access to a new table in the config: [commands]. -// TODO: Could add an `aliases` to `CustomTypableCommand` and then add those as well? - use std::{fmt::Write, sync::Arc}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -14,31 +8,7 @@ pub struct CustomTypeableCommands { impl Default for CustomTypeableCommands { fn default() -> Self { Self { - commands: vec![ - CustomTypableCommand { - name: Arc::from(":lg"), - desc: Some(Arc::from("runs lazygit in a floating pane")), - commands: vec![Arc::from(":sh wezterm cli spawn --floating-pane lazygit")] - .into(), - accepts: None, - completer: None, - }, - CustomTypableCommand { - name: Arc::from(":w"), - desc: Some(Arc::from("writes buffer forcefully and changes directory")), - commands: vec![ - Arc::from(":write --force %{arg}"), - Arc::from(":cd %sh{ %{arg} | path dirname }"), - Arc::from(":cd %sh{ %{arg} | path dirname }"), - Arc::from(":cd %sh{ %{arg} | path dirname }"), - Arc::from(":cd %sh{ %{arg} | path dirname }"), - ] - .into(), - accepts: Some(Arc::from("")), - completer: Some(Arc::from(":write")), - }, - ] - .into(), + commands: Arc::new([]), } } } @@ -49,15 +19,12 @@ impl CustomTypeableCommands { pub fn get(&self, name: &str) -> Option<&CustomTypableCommand> { self.commands .iter() - .find(|command| command.name.trim_start_matches(':') == name.trim_start_matches(':')) + .find(|command| command.name.as_ref() == name) } #[inline] pub fn names(&self) -> impl Iterator { - self.commands - .iter() - // ":wbc!" -> "wbc!" - .map(|command| command.name.as_ref()) + self.commands.iter().map(|command| command.name.as_ref()) } } @@ -78,7 +45,7 @@ impl CustomTypableCommand { // :write --force %{arg} -> :cd %sh{ %{arg} | path dirname } let mut prompt = String::new(); - prompt.push_str(self.name.trim_start_matches(':')); + prompt.push_str(self.name.as_ref()); if let Some(accepts) = &self.accepts { write!(prompt, " {accepts}").unwrap(); @@ -98,7 +65,7 @@ impl CustomTypableCommand { prompt.push_str(" "); for (idx, command) in self.commands.iter().enumerate() { - write!(prompt, ":{}", command.trim_start_matches(':')).unwrap(); + write!(prompt, ":{command}").unwrap(); if idx + 1 == self.commands.len() { break; @@ -126,8 +93,6 @@ impl CustomTypableCommand { } pub fn iter(&self) -> impl Iterator { - self.commands - .iter() - .map(|command| command.trim_start_matches(':')) + self.commands.iter().map(|command| command.as_ref()) } }