on master help sign in
find motion and textobj motion repeat (#891)
2ed01f2CossonLeo
helix-term/src/commands.rs+107−103
@@ -12,8 +12,13 @@ use helix_core::{
12};
12};
13
13
14use helix_view::{
14use helix_view::{
15− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
15+ clipboard::ClipboardType,
16+ document::Mode,
17+ editor::{Action, Motion},
18+ input::KeyEvent,
19+ keyboard::KeyCode,
20+ view::View,
16− view::View, Document, DocumentId, Editor, ViewId,
21+ Document, DocumentId, Editor, ViewId,
17};
22};
18
23
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)
667}
673}
668
674
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)
671where
676where
672 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
677 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
673{
678{
@@ -705,29 +710,48 @@ where
705 _ => return,
710 _ => return,
706 };
711 };
707
712
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);
716+ })));
717+ })
718+}
710
719
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 {
716− range.head - 1
717− } else {
718− range.head
719− };
720
721
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+
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)
726− }
751+ }
727− })
752+ })
728− });
753+ });
729− doc.set_selection(view.id, selection);
754+ doc.set_selection(view.id, selection);
730− })
731}
755}
732
756
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)
743 } else {
767 } else {
768+ let n = match text.get_char(pos) {
769+ Some(next_ch) if next_ch == ch => n + 1,
770+ _ => n,
771+ };
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))
745 }
773 }
746}
774}
@@ -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)
757 } else {
785 } 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+ };
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()))
759 }
791 }
760}
792}
761
793
762fn find_till_char(cx: &mut Context) {
794fn find_till_char(cx: &mut Context) {
763− find_char_impl(
764− cx,
765− find_next_char_impl,
795+ will_find_char(cx, find_next_char_impl, false, false)
766− false, /* inclusive */
767− false, /* extend */
768− )
769}
796}
770
797
771fn find_next_char(cx: &mut Context) {
798fn find_next_char(cx: &mut Context) {
772− find_char_impl(
773− cx,
774− find_next_char_impl,
799+ will_find_char(cx, find_next_char_impl, true, false)
775− true, /* inclusive */
776− false, /* extend */
777− )
778}
800}
779
801
780fn extend_till_char(cx: &mut Context) {
802fn extend_till_char(cx: &mut Context) {
781− find_char_impl(
782− cx,
783− find_next_char_impl,
803+ will_find_char(cx, find_next_char_impl, false, true)
784− false, /* inclusive */
785− true, /* extend */
786− )
787}
804}
788
805
789fn extend_next_char(cx: &mut Context) {
806fn extend_next_char(cx: &mut Context) {
790− find_char_impl(
791− cx,
792− find_next_char_impl,
807+ will_find_char(cx, find_next_char_impl, true, true)
793− true, /* inclusive */
794− true, /* extend */
795− )
796}
808}
797
809
798fn till_prev_char(cx: &mut Context) {
810fn till_prev_char(cx: &mut Context) {
799− find_char_impl(
800− cx,
801− find_prev_char_impl,
811+ will_find_char(cx, find_prev_char_impl, false, false)
802− false, /* inclusive */
803− false, /* extend */
804− )
805}
812}
806
813
807fn find_prev_char(cx: &mut Context) {
814fn find_prev_char(cx: &mut Context) {
808− find_char_impl(
809− cx,
810− find_prev_char_impl,
815+ will_find_char(cx, find_prev_char_impl, true, false)
811− true, /* inclusive */
812− false, /* extend */
813− )
814}
816}
815
817
816fn extend_till_prev_char(cx: &mut Context) {
818fn extend_till_prev_char(cx: &mut Context) {
817− find_char_impl(
818− cx,
819− find_prev_char_impl,
819+ will_find_char(cx, find_prev_char_impl, false, true)
820− false, /* inclusive */
821− true, /* extend */
822− )
823}
820}
824
821
825fn extend_prev_char(cx: &mut Context) {
822fn extend_prev_char(cx: &mut Context) {
826− find_char_impl(
827− cx,
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 {
829+ m.run(cx.editor);
830+ cx.editor.last_motion = last_motion;
831+ }
832}
832}
833
833
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(..);
4500−
4501+
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,
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+ )
4505 };
4516 };
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− };
4516
4517
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)
4527+ }
4528+ _ => range,
4526 }
4529 }
4527− _ => range,
4528− }
4529− });
4530+ });
4530− doc.set_selection(view.id, selection);
4531+ doc.set_selection(view.id, selection);
4532+ };
4533+ textobject(&mut cx.editor);
4534+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
4531 }
4535 }
4532 })
4536 })
4533}
4537}
helix-term/src/keymap.rs+1
@@ -395,6 +395,7 @@ impl Default for Keymaps {
395 "F" => find_prev_char,
395 "F" => find_prev_char,
396 "r" => replace,
396 "r" => replace,
397 "R" => replace_with_yanked,
397 "R" => replace_with_yanked,
398+ "A-." => repeat_last_motion,
398
399
399 "~" => switch_case,
400 "~" => switch_case,
400 "`" => switch_to_lowercase,
401 "`" => switch_to_lowercase,
helix-view/src/editor.rs+14
@@ -93,6 +93,18 @@ impl Default for Config {
93 }
93 }
94}
94}
95
95
96+pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
97+impl Motion {
98+ pub fn run(&self, e: &mut Editor) {
99+ (self.0)(e)
100+ }
101+}
102+impl std::fmt::Debug for Motion {
103+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104+ f.write_str("motion")
105+ }
106+}
107+
96#[derive(Debug)]
108#[derive(Debug)]
97pub struct Editor {
109pub struct Editor {
98 pub tree: Tree,
110 pub tree: Tree,
@@ -112,6 +124,7 @@ pub struct Editor {
112 pub config: Config,
124 pub config: Config,
113
125
114 pub idle_timer: Pin<Box<Sleep>>,
126 pub idle_timer: Pin<Box<Sleep>>,
127+ pub last_motion: Option<Motion>,
115}
128}
116
129
117#[derive(Debug, Copy, Clone)]
130#[derive(Debug, Copy, Clone)]
@@ -147,6 +160,7 @@ impl Editor {
147 clipboard_provider: get_clipboard_provider(),
160 clipboard_provider: get_clipboard_provider(),
148 status_msg: None,
161 status_msg: None,
149 idle_timer: Box::pin(sleep(config.idle_timeout)),
162 idle_timer: Box::pin(sleep(config.idle_timeout)),
163+ last_motion: None,
150 config,
164 config,
151 }
165 }
152 }
166 }