diff options
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r-- | helix-term/src/commands.rs | 158 |
1 files changed, 112 insertions, 46 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8149bd89..99d1432c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -134,47 +134,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) { view.offset.row = line.saturating_sub(relative); } -/// A command is composed of a static name, and a function that takes the current state plus a count, -/// and does a side-effect on the state (usually by creating and applying a transaction). -#[derive(Copy, Clone)] -pub struct Command { - name: &'static str, - fun: fn(cx: &mut Context), - doc: &'static str, -} - -macro_rules! commands { +/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like +/// :format. It causes a side-effect on the state (usually by creating and applying a transaction). +/// Both of these types of commands can be mapped with keybindings in the config.toml. +#[derive(Clone)] +pub enum MappableCommand { + Typable { + name: String, + args: Vec<String>, + doc: String, + }, + Static { + name: &'static str, + fun: fn(cx: &mut Context), + doc: &'static str, + }, +} + +macro_rules! static_commands { ( $($name:ident, $doc:literal,)* ) => { $( #[allow(non_upper_case_globals)] - pub const $name: Self = Self { + pub const $name: Self = Self::Static { name: stringify!($name), fun: $name, doc: $doc }; )* - pub const COMMAND_LIST: &'static [Self] = &[ + pub const STATIC_COMMAND_LIST: &'static [Self] = &[ $( Self::$name, )* ]; } } -impl Command { +impl MappableCommand { pub fn execute(&self, cx: &mut Context) { - (self.fun)(cx); + match &self { + MappableCommand::Typable { name, args, doc: _ } => { + let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect(); + if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) { + let mut cx = compositor::Context { + editor: cx.editor, + jobs: cx.jobs, + scroll: None, + }; + if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) { + cx.editor.set_error(format!("{}", e)); + } + } + } + MappableCommand::Static { fun, .. } => (fun)(cx), + } } - pub fn name(&self) -> &'static str { - self.name + pub fn name(&self) -> &str { + match &self { + MappableCommand::Typable { name, .. } => name, + MappableCommand::Static { name, .. } => name, + } } - pub fn doc(&self) -> &'static str { - self.doc + pub fn doc(&self) -> &str { + match &self { + MappableCommand::Typable { doc, .. } => doc, + MappableCommand::Static { doc, .. } => doc, + } } #[rustfmt::skip] - commands!( + static_commands!( no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", @@ -367,33 +396,51 @@ impl Command { ); } -impl fmt::Debug for Command { +impl fmt::Debug for MappableCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command { name, .. } = self; - f.debug_tuple("Command").field(name).finish() + f.debug_tuple("MappableCommand") + .field(&self.name()) + .finish() } } -impl fmt::Display for Command { +impl fmt::Display for MappableCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command { name, .. } = self; - f.write_str(name) + f.write_str(self.name()) } } -impl std::str::FromStr for Command { +impl std::str::FromStr for MappableCommand { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - Command::COMMAND_LIST - .iter() - .copied() - .find(|cmd| cmd.name == s) - .ok_or_else(|| anyhow!("No command named '{}'", s)) + if let Some(suffix) = s.strip_prefix(':') { + let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim()); + let name = typable_command + .next() + .ok_or_else(|| anyhow!("Expected typable command name"))?; + let args = typable_command + .map(|s| s.to_owned()) + .collect::<Vec<String>>(); + cmd::TYPABLE_COMMAND_MAP + .get(name) + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + doc: format!(":{} {:?}", cmd.name, args), + args, + }) + .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) + } else { + MappableCommand::STATIC_COMMAND_LIST + .iter() + .cloned() + .find(|cmd| cmd.name() == s) + .ok_or_else(|| anyhow!("No command named '{}'", s)) + } } } -impl<'de> Deserialize<'de> for Command { +impl<'de> Deserialize<'de> for MappableCommand { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -403,9 +450,27 @@ impl<'de> Deserialize<'de> for Command { } } -impl PartialEq for Command { +impl PartialEq for MappableCommand { fn eq(&self, other: &Self) -> bool { - self.name() == other.name() + match (self, other) { + ( + MappableCommand::Typable { + name: first_name, .. + }, + MappableCommand::Typable { + name: second_name, .. + }, + ) => first_name == second_name, + ( + MappableCommand::Static { + name: first_name, .. + }, + MappableCommand::Static { + name: second_name, .. + }, + ) => first_name == second_name, + _ => false, + } } } @@ -2843,15 +2908,16 @@ mod cmd { } ]; - pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| { - TYPABLE_COMMAND_LIST - .iter() - .flat_map(|cmd| { - std::iter::once((cmd.name, cmd)) - .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd))) - }) - .collect() - }); + pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> = + Lazy::new(|| { + TYPABLE_COMMAND_LIST + .iter() + .flat_map(|cmd| { + std::iter::once((cmd.name, cmd)) + .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd))) + }) + .collect() + }); } fn command_mode(cx: &mut Context) { @@ -2877,7 +2943,7 @@ fn command_mode(cx: &mut Context) { if let Some(cmd::TypableCommand { completer: Some(completer), .. - }) = cmd::COMMANDS.get(parts[0]) + }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { completer(part) .into_iter() @@ -2912,7 +2978,7 @@ fn command_mode(cx: &mut Context) { } // Handle typable commands - if let Some(cmd) = cmd::COMMANDS.get(parts[0]) { + if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { cx.editor.set_error(format!("{}", e)); } @@ -2925,7 +2991,7 @@ fn command_mode(cx: &mut Context) { prompt.doc_fn = Box::new(|input: &str| { let part = input.split(' ').next().unwrap_or_default(); - if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) { + if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) { return Some(doc); } |