diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 07485f9f..c698c641 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -12,8 +12,13 @@ use helix_core::{ }; use helix_view::{ - clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, - view::View, Document, DocumentId, Editor, ViewId, + clipboard::ClipboardType, + document::Mode, + editor::{Action, Motion}, + input::KeyEvent, + keyboard::KeyCode, + view::View, + Document, DocumentId, Editor, ViewId, }; use anyhow::{anyhow, bail, Context as _}; @@ -198,6 +203,7 @@ impl Command { find_prev_char, "Move to previous occurance of char", extend_till_prev_char, "Extend till previous occurance of char", extend_prev_char, "Extend to previous occurance of char", + repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)", replace, "Replace with new char", switch_case, "Switch (toggle) case", switch_to_uppercase, "Switch to uppercase", @@ -666,8 +672,7 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -#[inline] -fn find_char_impl(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) +fn will_find_char(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, { @@ -705,31 +710,50 @@ where _ => return, }; - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - // TODO: use `Range::cursor()` here instead. However, that works in terms of - // graphemes, whereas this function doesn't yet. So we're doing the same logic - // here, but just in terms of chars instead. - let search_start_pos = if range.anchor < range.head { - range.head - 1 - } else { - range.head - }; - - search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { - if extend { - range.put_cursor(text, pos, true) - } else { - Range::point(range.cursor(text)).put_cursor(text, pos, true) - } - }) - }); - doc.set_selection(view.id, selection); + find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); + cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| { + find_char_impl(editor, &search_fn, inclusive, true, ch, 1); + }))); }) } +// + +#[inline] +fn find_char_impl( + editor: &mut Editor, + search_fn: &F, + inclusive: bool, + extend: bool, + ch: char, + count: usize, +) where + F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, +{ + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + // TODO: use `Range::cursor()` here instead. However, that works in terms of + // graphemes, whereas this function doesn't yet. So we're doing the same logic + // here, but just in terms of chars instead. + let search_start_pos = if range.anchor < range.head { + range.head - 1 + } else { + range.head + }; + + search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { + if extend { + range.put_cursor(text, pos, true) + } else { + Range::point(range.cursor(text)).put_cursor(text, pos, true) + } + }) + }); + doc.set_selection(view.id, selection); +} + fn find_next_char_impl( text: RopeSlice, ch: char, @@ -741,6 +765,10 @@ fn find_next_char_impl( if inclusive { search::find_nth_next(text, ch, pos, n) } else { + let n = match text.get_char(pos) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1)) } } @@ -755,80 +783,52 @@ fn find_prev_char_impl( if inclusive { search::find_nth_prev(text, ch, pos, n) } else { + let n = match text.get_char(pos.saturating_sub(1)) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars())) } } fn find_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, false) } fn find_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, false) } fn extend_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, true) } fn extend_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, true) } fn till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, false) } fn find_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, false) } fn extend_till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, true) } fn extend_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, true) +} + +fn repeat_last_motion(cx: &mut Context) { + let last_motion = cx.editor.last_motion.take(); + if let Some(m) = &last_motion { + m.run(cx.editor); + cx.editor.last_motion = last_motion; + } } fn replace(cx: &mut Context) { @@ -4495,39 +4495,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let count = cx.count(); cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + 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, + 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, + ) }; - textobject::textobject_treesitter( - text, - range, - objtype, - obj_name, - syntax.tree().root_node(), - lang_config, - count, - ) - }; - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), - 'c' => textobject_treesitter("class", range), - 'f' => textobject_treesitter("function", range), - 'p' => textobject_treesitter("parameter", range), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) + let selection = doc.selection(view.id).clone().transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + 'c' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'p' => textobject_treesitter("parameter", range), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, } - _ => range, - } - }); - doc.set_selection(view.id, selection); + }); + doc.set_selection(view.id, selection); + }; + textobject(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(textobject))); } }) } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f877387c..495fe892 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -395,6 +395,7 @@ impl Default for Keymaps { "F" => find_prev_char, "r" => replace, "R" => replace_with_yanked, + "A-." => repeat_last_motion, "~" => switch_case, "`" => switch_to_lowercase, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 51fe8a42..09fc3334 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -93,6 +93,18 @@ impl Default for Config { } } +pub struct Motion(pub Box); +impl Motion { + pub fn run(&self, e: &mut Editor) { + (self.0)(e) + } +} +impl std::fmt::Debug for Motion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("motion") + } +} + #[derive(Debug)] pub struct Editor { pub tree: Tree, @@ -112,6 +124,7 @@ pub struct Editor { pub config: Config, pub idle_timer: Pin>, + pub last_motion: Option, } #[derive(Debug, Copy, Clone)] @@ -147,6 +160,7 @@ impl Editor { clipboard_provider: get_clipboard_provider(), status_msg: None, idle_timer: Box::pin(sleep(config.idle_timeout)), + last_motion: None, config, } }