@@ -12,8 +12,13 @@ use helix_core::{
14use helix_view::{
14use helix_view::{
15− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
15+ clipboard::ClipboardType,
17+ editor::{Action, Motion},
16− view::View, Document, DocumentId, Editor, ViewId,
21+ Document, DocumentId, Editor, ViewId,
19use anyhow::{anyhow, bail, Context as _};
24use anyhow::{anyhow, bail, Context as _};
@@ -198,6 +203,7 @@ impl Command {
198 find_prev_char, "Move to previous occurance of char",
203 find_prev_char, "Move to previous occurance of char",
199 extend_till_prev_char, "Extend till previous occurance of char",
204 extend_till_prev_char, "Extend till previous occurance of char",
200 extend_prev_char, "Extend to previous occurance of char",
205 extend_prev_char, "Extend to previous occurance of char",
206+ repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)",
201 replace, "Replace with new char",
207 replace, "Replace with new char",
202 switch_case, "Switch (toggle) case",
208 switch_case, "Switch (toggle) case",
203 switch_to_uppercase, "Switch to uppercase",
209 switch_to_uppercase, "Switch to uppercase",
@@ -666,8 +672,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
666 extend_word_impl(cx, movement::move_next_long_word_end)
672 extend_word_impl(cx, movement::move_next_long_word_end)
670−fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
675+fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
672 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
677 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
@@ -705,29 +710,48 @@ where
705 _ => return,
710 _ => return,
708− let (view, doc) = current!(cx.editor);
713+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
709− let text = doc.text().slice(..);
714+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
715+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
711− let selection = doc.selection(view.id).clone().transform(|range| {
720+//
712− // TODO: use `Range::cursor()` here instead. However, that works in terms of
713− // graphemes, whereas this function doesn't yet. So we're doing the same logic
714− // here, but just in terms of chars instead.
715− let search_start_pos = if range.anchor < range.head {
723+fn find_char_impl<F>(
724+ editor: &mut Editor,
731+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
733+ let (view, doc) = current!(editor);
734+ let text = doc.text().slice(..);
736+ let selection = doc.selection(view.id).clone().transform(|range| {
737+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
738+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
739+ // here, but just in terms of chars instead.
740+ let search_start_pos = if range.anchor < range.head {
721− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
746+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
722− if extend {
747+ if extend {
723− range.put_cursor(text, pos, true)
748+ range.put_cursor(text, pos, true)
724− } else {
749+ } else {
725− Range::point(range.cursor(text)).put_cursor(text, pos, true)
750+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
729− doc.set_selection(view.id, selection);
754+ doc.set_selection(view.id, selection);
733fn find_next_char_impl(
757fn find_next_char_impl(
@@ -741,6 +765,10 @@ fn find_next_char_impl(
741 if inclusive {
765 if inclusive {
742 search::find_nth_next(text, ch, pos, n)
766 search::find_nth_next(text, ch, pos, n)
768+ let n = match text.get_char(pos) {
769+ Some(next_ch) if next_ch == ch => n + 1,
744 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
772 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
@@ -755,80 +783,52 @@ fn find_prev_char_impl(
755 if inclusive {
783 if inclusive {
756 search::find_nth_prev(text, ch, pos, n)
784 search::find_nth_prev(text, ch, pos, n)
786+ let n = match text.get_char(pos.saturating_sub(1)) {
787+ Some(next_ch) if next_ch == ch => n + 1,
758 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
790 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
762fn find_till_char(cx: &mut Context) {
794fn find_till_char(cx: &mut Context) {
765− find_next_char_impl,
795+ will_find_char(cx, find_next_char_impl, false, false)
766− false, /* inclusive */
771fn find_next_char(cx: &mut Context) {
798fn find_next_char(cx: &mut Context) {
774− find_next_char_impl,
799+ will_find_char(cx, find_next_char_impl, true, false)
775− true, /* inclusive */
780fn extend_till_char(cx: &mut Context) {
802fn extend_till_char(cx: &mut Context) {
783− find_next_char_impl,
803+ will_find_char(cx, find_next_char_impl, false, true)
784− false, /* inclusive */
789fn extend_next_char(cx: &mut Context) {
806fn extend_next_char(cx: &mut Context) {
792− find_next_char_impl,
807+ will_find_char(cx, find_next_char_impl, true, true)
793− true, /* inclusive */
798fn till_prev_char(cx: &mut Context) {
810fn till_prev_char(cx: &mut Context) {
801− find_prev_char_impl,
811+ will_find_char(cx, find_prev_char_impl, false, false)
802− false, /* inclusive */
807fn find_prev_char(cx: &mut Context) {
814fn find_prev_char(cx: &mut Context) {
810− find_prev_char_impl,
815+ will_find_char(cx, find_prev_char_impl, true, false)
811− true, /* inclusive */
816fn extend_till_prev_char(cx: &mut Context) {
818fn extend_till_prev_char(cx: &mut Context) {
819− find_prev_char_impl,
819+ will_find_char(cx, find_prev_char_impl, false, true)
820− false, /* inclusive */
825fn extend_prev_char(cx: &mut Context) {
822fn extend_prev_char(cx: &mut Context) {
828− find_prev_char_impl,
823+ will_find_char(cx, find_prev_char_impl, true, true)
829− true, /* inclusive */
824+}
830− true, /* extend */
825+
831− )
826+fn repeat_last_motion(cx: &mut Context) {
827+ let last_motion = cx.editor.last_motion.take();
828+ if let Some(m) = &last_motion {
830+ cx.editor.last_motion = last_motion;
834fn replace(cx: &mut Context) {
834fn replace(cx: &mut Context) {
@@ -4495,39 +4495,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4495 let count = cx.count();
4495 let count = cx.count();
4496 cx.on_next_key(move |cx, event| {
4496 cx.on_next_key(move |cx, event| {
4497 if let Some(ch) = event.char() {
4497 if let Some(ch) = event.char() {
4498+ let textobject = move |editor: &mut Editor| {
4498− let (view, doc) = current!(cx.editor);
4499+ let (view, doc) = current!(editor);
4499− let text = doc.text().slice(..);
4500+ let text = doc.text().slice(..);
4501− let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4502+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4502− let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4503+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4503− Some(t) => t,
4504+ Some(t) => t,
4504− None => return range,
4505+ None => return range,
4507+ textobject::textobject_treesitter(
4512+ syntax.tree().root_node(),
4506− textobject::textobject_treesitter(
4511− syntax.tree().root_node(),
4517− let selection = doc.selection(view.id).clone().transform(|range| {
4518+ let selection = doc.selection(view.id).clone().transform(|range| {
4518− match ch {
4519+ match ch {
4519− 'w' => textobject::textobject_word(text, range, objtype, count),
4520+ 'w' => textobject::textobject_word(text, range, objtype, count),
4520− 'c' => textobject_treesitter("class", range),
4521+ 'c' => textobject_treesitter("class", range),
4521− 'f' => textobject_treesitter("function", range),
4522+ 'f' => textobject_treesitter("function", range),
4522− 'p' => textobject_treesitter("parameter", range),
4523+ 'p' => textobject_treesitter("parameter", range),
4523− // TODO: cancel new ranges if inconsistent surround matches across lines
4524+ // TODO: cancel new ranges if inconsistent surround matches across lines
4524− ch if !ch.is_ascii_alphanumeric() => {
4525+ ch if !ch.is_ascii_alphanumeric() => {
4525− textobject::textobject_surround(text, range, objtype, ch, count)
4526+ textobject::textobject_surround(text, range, objtype, ch, count)
4530− doc.set_selection(view.id, selection);
4531+ doc.set_selection(view.id, selection);
4533+ textobject(&mut cx.editor);
4534+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));