@@ -12,8 +12,13 @@ use helix_core::{
1212};
1313
1414use helix_view::{
15− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
16− view::View, Document, DocumentId, Editor, ViewId,
15+ clipboard::ClipboardType,
16+ document::Mode,
17+ editor::{Action, Motion},
18+ input::KeyEvent,
19+ keyboard::KeyCode,
20+ view::View,
21+ Document, DocumentId, Editor, ViewId,
1722};
1823
1924use anyhow::{anyhow, bail, Context as _};
@@ -198,6 +203,7 @@ impl Command {
198203 find_prev_char, "Move to previous occurance of char",
199204 extend_till_prev_char, "Extend till previous occurance of char",
200205 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...)",
201207 replace, "Replace with new char",
202208 switch_case, "Switch (toggle) case",
203209 switch_to_uppercase, "Switch to uppercase",
@@ -666,8 +672,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
666672 extend_word_impl(cx, movement::move_next_long_word_end)
667673}
668674
669−#[inline]
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)
671676where
672677 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
673678{
@@ -705,29 +710,48 @@ where
705710 _ => return,
706711 };
707712
708− let (view, doc) = current!(cx.editor);
709− let text = doc.text().slice(..);
713+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
714+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
715+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
716+ })));
717+ })
718+}
710719
711− let selection = doc.selection(view.id).clone().transform(|range| {
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 {
716− range.head - 1
717− } else {
718− range.head
719− };
720+//
720721
721− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
722− if extend {
723− range.put_cursor(text, pos, true)
724− } else {
725− Range::point(range.cursor(text)).put_cursor(text, pos, true)
726− }
727− })
728− });
729− doc.set_selection(view.id, selection);
730− })
722+#[inline]
723+fn find_char_impl<F>(
724+ editor: &mut Editor,
725+ search_fn: &F,
726+ inclusive: bool,
727+ extend: bool,
728+ ch: char,
729+ count: usize,
730+) where
731+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
732+{
733+ let (view, doc) = current!(editor);
734+ let text = doc.text().slice(..);
735+
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 {
741+ range.head - 1
742+ } else {
743+ range.head
744+ };
745+
746+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
747+ if extend {
748+ range.put_cursor(text, pos, true)
749+ } else {
750+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
751+ }
752+ })
753+ });
754+ doc.set_selection(view.id, selection);
731755}
732756
733757fn find_next_char_impl(
@@ -741,6 +765,10 @@ fn find_next_char_impl(
741765 if inclusive {
742766 search::find_nth_next(text, ch, pos, n)
743767 } else {
768+ let n = match text.get_char(pos) {
769+ Some(next_ch) if next_ch == ch => n + 1,
770+ _ => n,
771+ };
744772 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
745773 }
746774}
@@ -755,80 +783,52 @@ fn find_prev_char_impl(
755783 if inclusive {
756784 search::find_nth_prev(text, ch, pos, n)
757785 } else {
786+ let n = match text.get_char(pos.saturating_sub(1)) {
787+ Some(next_ch) if next_ch == ch => n + 1,
788+ _ => n,
789+ };
758790 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
759791 }
760792}
761793
762794fn find_till_char(cx: &mut Context) {
763− find_char_impl(
764− cx,
765− find_next_char_impl,
766− false, /* inclusive */
767− false, /* extend */
768− )
795+ will_find_char(cx, find_next_char_impl, false, false)
769796}
770797
771798fn find_next_char(cx: &mut Context) {
772− find_char_impl(
773− cx,
774− find_next_char_impl,
775− true, /* inclusive */
776− false, /* extend */
777− )
799+ will_find_char(cx, find_next_char_impl, true, false)
778800}
779801
780802fn extend_till_char(cx: &mut Context) {
781− find_char_impl(
782− cx,
783− find_next_char_impl,
784− false, /* inclusive */
785− true, /* extend */
786− )
803+ will_find_char(cx, find_next_char_impl, false, true)
787804}
788805
789806fn extend_next_char(cx: &mut Context) {
790− find_char_impl(
791− cx,
792− find_next_char_impl,
793− true, /* inclusive */
794− true, /* extend */
795− )
807+ will_find_char(cx, find_next_char_impl, true, true)
796808}
797809
798810fn till_prev_char(cx: &mut Context) {
799− find_char_impl(
800− cx,
801− find_prev_char_impl,
802− false, /* inclusive */
803− false, /* extend */
804− )
811+ will_find_char(cx, find_prev_char_impl, false, false)
805812}
806813
807814fn find_prev_char(cx: &mut Context) {
808− find_char_impl(
809− cx,
810− find_prev_char_impl,
811− true, /* inclusive */
812− false, /* extend */
813− )
815+ will_find_char(cx, find_prev_char_impl, true, false)
814816}
815817
816818fn extend_till_prev_char(cx: &mut Context) {
817− find_char_impl(
818− cx,
819− find_prev_char_impl,
820− false, /* inclusive */
821− true, /* extend */
822− )
819+ will_find_char(cx, find_prev_char_impl, false, true)
823820}
824821
825822fn extend_prev_char(cx: &mut Context) {
826− find_char_impl(
827− cx,
828− find_prev_char_impl,
829− true, /* inclusive */
830− true, /* extend */
831− )
823+ will_find_char(cx, find_prev_char_impl, true, true)
824+}
825+
826+fn repeat_last_motion(cx: &mut Context) {
827+ let last_motion = cx.editor.last_motion.take();
828+ if let Some(m) = &last_motion {
829+ m.run(cx.editor);
830+ cx.editor.last_motion = last_motion;
831+ }
832832}
833833
834834fn replace(cx: &mut Context) {
@@ -4495,39 +4495,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
44954495 let count = cx.count();
44964496 cx.on_next_key(move |cx, event| {
44974497 if let Some(ch) = event.char() {
4498− let (view, doc) = current!(cx.editor);
4499− let text = doc.text().slice(..);
4500−
4501− let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4502− let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4503− Some(t) => t,
4504− None => return range,
4498+ let textobject = move |editor: &mut Editor| {
4499+ let (view, doc) = current!(editor);
4500+ let text = doc.text().slice(..);
4501+
4502+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4503+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4504+ Some(t) => t,
4505+ None => return range,
4506+ };
4507+ textobject::textobject_treesitter(
4508+ text,
4509+ range,
4510+ objtype,
4511+ obj_name,
4512+ syntax.tree().root_node(),
4513+ lang_config,
4514+ count,
4515+ )
45054516 };
4506− textobject::textobject_treesitter(
4507− text,
4508− range,
4509− objtype,
4510− obj_name,
4511− syntax.tree().root_node(),
4512− lang_config,
4513− count,
4514− )
4515− };
45164517
4517− let selection = doc.selection(view.id).clone().transform(|range| {
4518− match ch {
4519− 'w' => textobject::textobject_word(text, range, objtype, count),
4520− 'c' => textobject_treesitter("class", range),
4521− 'f' => textobject_treesitter("function", range),
4522− 'p' => textobject_treesitter("parameter", range),
4523− // TODO: cancel new ranges if inconsistent surround matches across lines
4524− ch if !ch.is_ascii_alphanumeric() => {
4525− textobject::textobject_surround(text, range, objtype, ch, count)
4518+ let selection = doc.selection(view.id).clone().transform(|range| {
4519+ match ch {
4520+ 'w' => textobject::textobject_word(text, range, objtype, count),
4521+ 'c' => textobject_treesitter("class", range),
4522+ 'f' => textobject_treesitter("function", range),
4523+ 'p' => textobject_treesitter("parameter", range),
4524+ // TODO: cancel new ranges if inconsistent surround matches across lines
4525+ ch if !ch.is_ascii_alphanumeric() => {
4526+ textobject::textobject_surround(text, range, objtype, ch, count)
4527+ }
4528+ _ => range,
45264529 }
4527− _ => range,
4528− }
4529− });
4530− doc.set_selection(view.id, selection);
4530+ });
4531+ doc.set_selection(view.id, selection);
4532+ };
4533+ textobject(&mut cx.editor);
4534+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
45314535 }
45324536 })
45334537}