diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6e037a471..4b1698e45 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -521,8 +521,28 @@ impl MappableCommand { surround_add, "Surround add", surround_replace, "Surround replace", surround_delete, "Surround delete", - select_textobject_around, "Select around object", - select_textobject_inner, "Select inside object", + select_textobject_inside_type, "Select inside type definition (tree-sitter)", + select_textobject_around_type, "Select around type definition (tree-sitter)", + select_textobject_inside_function, "Select inside function (tree-sitter)", + select_textobject_around_function, "Select around function (tree-sitter)", + select_textobject_inside_parameter, "Select inside argument/parameter (tree-sitter)", + select_textobject_around_parameter, "Select around argument/parameter (tree-sitter)", + select_textobject_inside_comment, "Select inside comment (tree-sitter)", + select_textobject_around_comment, "Select around comment (tree-sitter)", + select_textobject_inside_test, "Select inside test (tree-sitter)", + select_textobject_around_test, "Select around test (tree-sitter)", + select_textobject_inside_entry, "Select inside data structure entry (tree-sitter)", + select_textobject_around_entry, "Select around data structure entry (tree-sitter)", + select_textobject_inside_paragraph, "Select inside paragraph", + select_textobject_around_paragraph, "Select around paragraph", + select_textobject_inside_closest_surrounding_pair, "Select inside closest surrounding pair (tree-sitter)", + select_textobject_around_closest_surrounding_pair, "Select around closest surrounding pair (tree-sitter)", + select_textobject_inside_word, "Select inside word", + select_textobject_around_word, "Select around word", + select_textobject_inside_WORD, "Select inside WORD", + select_textobject_around_WORD, "Select around WORD", + select_textobject_inside_change, "Select inside VCS change", + select_textobject_around_change, "Select around VCS change", goto_next_function, "Goto next function", goto_prev_function, "Goto previous function", goto_next_class, "Goto next type definition", @@ -669,6 +689,47 @@ impl PartialEq for MappableCommand { } } +// TODO: this is mostly a copy of MappableCommand. Fold this into MappableCommand? +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FallbackCommand { + name: &'static str, + fun: fn(cx: &mut Context, ch: char), + doc: &'static str, +} + +macro_rules! static_fallback_commands { + ( $($name:ident, $doc:literal,)* ) => { + $( + #[allow(non_upper_case_globals)] + pub const $name: Self = Self { + name: stringify!($name), + fun: $name, + doc: $doc + }; + )* + + pub const FALLBACK_COMMAND_LIST: &'static [Self] = &[ + $( Self::$name, )* + ]; + } +} + +impl FallbackCommand { + pub fn execute(&self, cx: &mut Context, ch: char) { + (self.fun)(cx, ch) + } + + pub fn doc(&self) -> &str { + self.doc + } + + #[rustfmt::skip] + static_fallback_commands!( + select_textobject_inside_surrounding_pair, "Select inside any character acting as a pair (tree-sitter)", + select_textobject_around_surrounding_pair, "Select around any character acting as a pair (tree-sitter)", + ); +} + fn no_op(_cx: &mut Context) {} type MoveFn = @@ -5440,119 +5501,225 @@ fn goto_prev_entry(cx: &mut Context) { goto_ts_object_impl(cx, "entry", Direction::Backward) } -fn select_textobject_around(cx: &mut Context) { - select_textobject(cx, textobject::TextObject::Around); +fn select_textobject_inside_type(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "class"); +} + +fn select_textobject_around_type(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "class"); +} + +fn select_textobject_inside_function(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "function"); +} + +fn select_textobject_around_function(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "function"); +} + +fn select_textobject_inside_parameter(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "parameter"); +} + +fn select_textobject_around_parameter(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "parameter"); +} + +fn select_textobject_inside_comment(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "comment"); +} + +fn select_textobject_around_comment(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "comment"); +} + +fn select_textobject_inside_test(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "test"); } -fn select_textobject_inner(cx: &mut Context) { - select_textobject(cx, textobject::TextObject::Inside); +fn select_textobject_around_test(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "test"); } -fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { +fn select_textobject_inside_entry(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Inside, "entry"); +} + +fn select_textobject_around_entry(cx: &mut Context) { + textobject_treesitter(cx, textobject::TextObject::Around, "entry"); +} + +fn textobject_treesitter( + cx: &mut Context, + obj_type: textobject::TextObject, + object_name: &'static str, +) { let count = cx.count(); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => { + editor.set_status("Syntax information is not available in current buffer"); + return; + } + }; + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + textobject::textobject_treesitter( + text, + range, + obj_type, + object_name, + syntax.tree().root_node(), + lang_config, + count, + ) + }); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(motion); +} - cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; - if let Some(ch) = event.char() { - let textobject = move |editor: &mut Editor| { - let (view, doc) = current!(editor); - let text = doc.text().slice(..); - - let textobject_treesitter = |obj_name: &str, range: Range| -> Range { - let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { - Some(t) => t, - None => return range, - }; - textobject::textobject_treesitter( - text, - range, - objtype, - obj_name, - syntax.tree().root_node(), - lang_config, - count, - ) - }; +fn select_textobject_inside_paragraph(cx: &mut Context) { + textobject_paragraph(cx, textobject::TextObject::Inside); +} - if ch == 'g' && doc.diff_handle().is_none() { - editor.set_status("Diff is not available in current buffer"); - return; - } +fn select_textobject_around_paragraph(cx: &mut Context) { + textobject_paragraph(cx, textobject::TextObject::Around); +} - let textobject_change = |range: Range| -> Range { - let diff_handle = doc.diff_handle().unwrap(); - let diff = diff_handle.load(); - let line = range.cursor_line(text); - let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { - hunk_idx - } else { - return range; - }; - let hunk = diff.nth_hunk(hunk_idx).after; +fn textobject_paragraph(cx: &mut Context, textobject: textobject::TextObject) { + let count = cx.count(); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| textobject::textobject_paragraph(text, range, textobject, count)); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(motion); +} - let start = text.line_to_char(hunk.start as usize); - let end = text.line_to_char(hunk.end as usize); - Range::new(start, end).with_direction(range.direction()) - }; +fn select_textobject_inside_closest_surrounding_pair(cx: &mut Context) { + textobject_closest_surrounding_pair(cx, textobject::TextObject::Inside); +} - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count, false), - 'W' => textobject::textobject_word(text, range, objtype, count, true), - 't' => textobject_treesitter("class", range), - 'f' => textobject_treesitter("function", range), - 'a' => textobject_treesitter("parameter", range), - 'c' => textobject_treesitter("comment", range), - 'T' => textobject_treesitter("test", range), - 'e' => textobject_treesitter("entry", range), - 'p' => textobject::textobject_paragraph(text, range, objtype, count), - 'm' => textobject::textobject_pair_surround_closest( - doc.syntax(), - text, - range, - objtype, - count, - ), - 'g' => textobject_change(range), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround( - doc.syntax(), - text, - range, - objtype, - ch, - count, - ), - _ => range, - } - }); - doc.set_selection(view.id, selection); +fn select_textobject_around_closest_surrounding_pair(cx: &mut Context) { + textobject_closest_surrounding_pair(cx, textobject::TextObject::Around); +} + +fn textobject_closest_surrounding_pair(cx: &mut Context, textobject: textobject::TextObject) { + let count = cx.count(); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + let syntax = doc.syntax(); + let selection = doc.selection(view.id).clone().transform(|range| { + textobject::textobject_pair_surround_closest(syntax, text, range, textobject, count) + }); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(motion); +} + +fn select_textobject_inside_word(cx: &mut Context) { + textobject_word(cx, textobject::TextObject::Inside, false); +} + +fn select_textobject_around_word(cx: &mut Context) { + textobject_word(cx, textobject::TextObject::Around, false); +} + +#[allow(non_snake_case)] +fn select_textobject_inside_WORD(cx: &mut Context) { + textobject_word(cx, textobject::TextObject::Inside, true); +} + +#[allow(non_snake_case)] +fn select_textobject_around_WORD(cx: &mut Context) { + textobject_word(cx, textobject::TextObject::Around, true); +} + +fn textobject_word(cx: &mut Context, textobject: textobject::TextObject, longword: bool) { + let count = cx.count(); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + textobject::textobject_word(text, range, textobject, count, longword) + }); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(motion); +} + +fn select_textobject_inside_change(cx: &mut Context) { + textobject_change(cx); +} + +fn select_textobject_around_change(cx: &mut Context) { + textobject_change(cx); +} + +fn textobject_change(cx: &mut Context) { + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let Some(diff_handle) = doc.diff_handle() else { + editor.set_status("Diff is not available in current buffer"); + return; + }; + let diff = diff_handle.load(); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + let line = range.cursor_line(text); + let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { + hunk_idx + } else { + return range; }; - cx.editor.apply_motion(textobject); - } - }); + let hunk = diff.nth_hunk(hunk_idx).after; - let title = match objtype { - textobject::TextObject::Inside => "Match inside", - textobject::TextObject::Around => "Match around", - _ => return, + let start = text.line_to_char(hunk.start as usize); + let end = text.line_to_char(hunk.end as usize); + Range::new(start, end).with_direction(range.direction()) + }); + drop(diff); + doc.set_selection(view.id, selection); }; - let help_text = [ - ("w", "Word"), - ("W", "WORD"), - ("p", "Paragraph"), - ("t", "Type definition (tree-sitter)"), - ("f", "Function (tree-sitter)"), - ("a", "Argument/parameter (tree-sitter)"), - ("c", "Comment (tree-sitter)"), - ("T", "Test (tree-sitter)"), - ("e", "Data structure entry (tree-sitter)"), - ("m", "Closest surrounding pair (tree-sitter)"), - ("g", "Change"), - (" ", "... or any character acting as a pair"), - ]; + cx.editor.apply_motion(motion); +} + +fn select_textobject_inside_surrounding_pair(cx: &mut Context, ch: char) { + textobject_surrounding_pair(cx, textobject::TextObject::Inside, ch); +} + +fn select_textobject_around_surrounding_pair(cx: &mut Context, ch: char) { + textobject_surrounding_pair(cx, textobject::TextObject::Around, ch); +} + +fn textobject_surrounding_pair( + cx: &mut Context, + textobject: textobject::TextObject, + pair_char: char, +) { + if pair_char.is_ascii_alphanumeric() { + return; + } - cx.editor.autoinfo = Some(Info::new(title, &help_text)); + let count = cx.count(); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + let syntax = doc.syntax(); + let selection = doc.selection(view.id).clone().transform(|range| { + textobject::textobject_pair_surround(syntax, text, range, textobject, pair_char, count) + }); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(motion); } fn surround_add(cx: &mut Context) { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 020ecaf40..c9931e26c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,6 +1,7 @@ pub mod default; pub mod macros; +use crate::commands::FallbackCommand; pub use crate::commands::MappableCommand; use arc_swap::{ access::{DynAccess, DynGuard}, @@ -24,7 +25,8 @@ pub struct KeyTrieNode { name: String, map: HashMap, order: Vec, - pub is_sticky: bool, + is_sticky: bool, + fallback: Option, } impl<'de> Deserialize<'de> for KeyTrieNode { @@ -49,6 +51,7 @@ impl KeyTrieNode { map, order, is_sticky: false, + fallback: None, } } @@ -99,13 +102,16 @@ impl KeyTrieNode { .unwrap() }); - let body: Vec<_> = body + let mut body: Vec<_> = body .into_iter() .map(|(events, desc)| { let events = events.iter().map(ToString::to_string).collect::>(); (events.join(", "), desc) }) .collect(); + if let Some(fallback) = self.fallback.as_ref() { + body.push(("...".to_string(), fallback.doc())); + } Info::new(&self.name, &body) } } @@ -267,6 +273,28 @@ impl KeyTrie { } Some(trie) } + + pub fn search_fallback(&self, keys: &[KeyEvent]) -> Option<&FallbackCommand> { + // TODO: this is copied from above, hacky + let mut trie = self; + let mut keys = keys.iter().peekable(); + while let Some(key) = keys.next() { + trie = match trie { + KeyTrie::Node(map) => match map.get(key) { + Some(i) => Some(i), + None => { + if keys.peek().is_none() { + return map.fallback.as_ref(); + } + None + } + }, + // leaf encountered while keys left to process + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, + }? + } + None + } } #[derive(Debug, Clone, PartialEq)] @@ -281,6 +309,7 @@ pub enum KeymapResult { /// Key is invalid in combination with previous keys. Contains keys leading upto /// and including current (invalid) key. Cancelled(Vec), + Fallback(FallbackCommand, char), } /// A map of command names to keybinds that will execute the command. @@ -376,7 +405,16 @@ impl Keymaps { self.state.clear(); KeymapResult::MatchedSequence(cmds.clone()) } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), + None => { + if let Some(ch) = key.char() { + if let Some(fallback) = trie.search_fallback(&self.state[1..]) { + self.state.clear(); + return KeymapResult::Fallback(fallback.clone(), ch); + } + } + + KeymapResult::Cancelled(self.state.drain(..).collect()) + } } } } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5a3e8eed4..fcb1710c3 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -104,8 +104,32 @@ pub fn default() -> HashMap { "s" => surround_add, "r" => surround_replace, "d" => surround_delete, - "a" => select_textobject_around, - "i" => select_textobject_inner, + "i" => { "Match inside" fallback=select_textobject_inside_surrounding_pair + "w" => select_textobject_inside_word, + "W" => select_textobject_inside_WORD, + "p" => select_textobject_inside_paragraph, + "t" => select_textobject_inside_type, + "f" => select_textobject_inside_function, + "a" => select_textobject_inside_parameter, + "c" => select_textobject_inside_comment, + "T" => select_textobject_inside_test, + "e" => select_textobject_inside_entry, + "m" => select_textobject_inside_closest_surrounding_pair, + "g" => select_textobject_inside_change, + }, + "a" => { "Match around" fallback=select_textobject_around_surrounding_pair + "w" => select_textobject_around_word, + "W" => select_textobject_around_WORD, + "p" => select_textobject_around_paragraph, + "t" => select_textobject_around_type, + "f" => select_textobject_around_function, + "a" => select_textobject_around_parameter, + "c" => select_textobject_around_comment, + "T" => select_textobject_around_test, + "e" => select_textobject_around_entry, + "m" => select_textobject_around_closest_surrounding_pair, + "g" => select_textobject_around_change, + }, }, "[" => { "Left bracket" "d" => goto_prev_diag, diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 15d2aa53b..13d261449 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -84,9 +84,9 @@ macro_rules! keymap { }; (@trie - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $(fallback=$fallback:ident)? $($($key:literal)|+ => $value:tt,)+ } ) => { - keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) + keymap!({ $label $(sticky=$sticky)? $(fallback=$fallback)? $($($key)|+ => $value,)+ }) }; (@trie [$($cmd:ident),* $(,)?]) => { @@ -94,7 +94,7 @@ macro_rules! keymap { }; ( - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $(fallback=$fallback:ident)? $($($key:literal)|+ => $value:tt,)+ } ) => { // modified from the hashmap! macro { @@ -113,6 +113,7 @@ macro_rules! keymap { )+ )* let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + $( _node.fallback = Some($crate::commands::FallbackCommand::$fallback); )? $( _node.is_sticky = $sticky; )? $crate::keymap::KeyTrie::Node(_node) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f7541fe25..81144e667 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -909,6 +909,9 @@ impl EditorView { } } KeymapResult::NotFound | KeymapResult::Cancelled(_) => return Some(key_result), + KeymapResult::Fallback(fallback, ch) => { + fallback.execute(cxt, *ch); + } } None }