@@ -91,6 +91,16 @@ pub enum Motion {
mode: FindRange,
smartcase: bool,
},
+ Sneak {
+ first_char: char,
+ second_char: char,
+ smartcase: bool,
+ },
+ SneakBackward {
+ first_char: char,
+ second_char: char,
+ smartcase: bool,
+ },
RepeatFind {
last_find: Box<Motion>,
},
@@ -538,8 +548,10 @@ impl Vim {
}
pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
- if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
- self.active_operator()
+ if let Some(Operator::FindForward { .. })
+ | Some(Operator::Sneak { .. })
+ | Some(Operator::SneakBackward { .. })
+ | Some(Operator::FindBackward { .. }) = self.active_operator()
{
self.pop_operator(cx);
}
@@ -625,6 +637,8 @@ impl Motion {
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
+ | Sneak { .. }
+ | SneakBackward { .. }
| RepeatFind { .. }
| RepeatFindReversed { .. }
| Jump { line: false, .. }
@@ -666,6 +680,8 @@ impl Motion {
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
+ | Sneak { .. }
+ | SneakBackward { .. }
| RepeatFindReversed { .. }
| WindowTop
| WindowMiddle
@@ -727,6 +743,8 @@ impl Motion {
| PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
+ | Sneak { .. }
+ | SneakBackward { .. }
| Jump { .. }
| NextSectionStart
| NextSectionEnd
@@ -862,6 +880,22 @@ impl Motion {
find_backward(map, point, *after, *char, times, *mode, *smartcase),
SelectionGoal::None,
),
+ Sneak {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ return sneak(map, point, *first_char, *second_char, times, *smartcase)
+ .map(|new_point| (new_point, SelectionGoal::None));
+ }
+ SneakBackward {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
+ .map(|new_point| (new_point, SelectionGoal::None));
+ }
// ; -- repeat the last find done with t, f, T, F
RepeatFind { last_find } => match **last_find {
Motion::FindForward {
@@ -895,9 +929,44 @@ impl Motion {
(new_point, SelectionGoal::None)
}
+ Motion::Sneak {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ let mut new_point =
+ sneak(map, point, first_char, second_char, times, smartcase);
+ if new_point == Some(point) {
+ new_point =
+ sneak(map, point, first_char, second_char, times + 1, smartcase);
+ }
+
+ return new_point.map(|new_point| (new_point, SelectionGoal::None));
+ }
+
+ Motion::SneakBackward {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ let mut new_point =
+ sneak_backward(map, point, first_char, second_char, times, smartcase);
+ if new_point == Some(point) {
+ new_point = sneak_backward(
+ map,
+ point,
+ first_char,
+ second_char,
+ times + 1,
+ smartcase,
+ );
+ }
+
+ return new_point.map(|new_point| (new_point, SelectionGoal::None));
+ }
_ => return None,
},
- // , -- repeat the last find done with t, f, T, F, in opposite direction
+ // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
RepeatFindReversed { last_find } => match **last_find {
Motion::FindForward {
before,
@@ -930,6 +999,42 @@ impl Motion {
return new_point.map(|new_point| (new_point, SelectionGoal::None));
}
+
+ Motion::Sneak {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ let mut new_point =
+ sneak_backward(map, point, first_char, second_char, times, smartcase);
+ if new_point == Some(point) {
+ new_point = sneak_backward(
+ map,
+ point,
+ first_char,
+ second_char,
+ times + 1,
+ smartcase,
+ );
+ }
+
+ return new_point.map(|new_point| (new_point, SelectionGoal::None));
+ }
+
+ Motion::SneakBackward {
+ first_char,
+ second_char,
+ smartcase,
+ } => {
+ let mut new_point =
+ sneak(map, point, first_char, second_char, times, smartcase);
+ if new_point == Some(point) {
+ new_point =
+ sneak(map, point, first_char, second_char, times + 1, smartcase);
+ }
+
+ return new_point.map(|new_point| (new_point, SelectionGoal::None));
+ }
_ => return None,
},
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
@@ -2134,6 +2239,74 @@ fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
}
}
+fn sneak(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ first_target: char,
+ second_target: char,
+ times: usize,
+ smartcase: bool,
+) -> Option<DisplayPoint> {
+ let mut to = from;
+ let mut found = false;
+
+ for _ in 0..times {
+ found = false;
+ let new_to = find_boundary(
+ map,
+ movement::right(map, to),
+ FindRange::MultiLine,
+ |left, right| {
+ found = is_character_match(first_target, left, smartcase)
+ && is_character_match(second_target, right, smartcase);
+ found
+ },
+ );
+ if to == new_to {
+ break;
+ }
+ to = new_to;
+ }
+
+ if found {
+ Some(movement::left(map, to))
+ } else {
+ None
+ }
+}
+
+fn sneak_backward(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ first_target: char,
+ second_target: char,
+ times: usize,
+ smartcase: bool,
+) -> Option<DisplayPoint> {
+ let mut to = from;
+ let mut found = false;
+
+ for _ in 0..times {
+ found = false;
+ let new_to =
+ find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
+ found = is_character_match(first_target, left, smartcase)
+ && is_character_match(second_target, right, smartcase);
+ found
+ });
+ if to == new_to {
+ break;
+ }
+ to = new_to;
+ }
+
+ if found {
+ Some(movement::left(map, to))
+ } else {
+ None
+ }
+}
+
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
first_non_whitespace(map, false, correct_line)
@@ -17,7 +17,12 @@ use indoc::indoc;
use search::BufferSearchBar;
use workspace::WorkspaceSettings;
-use crate::{insert::NormalBefore, motion, state::Mode};
+use crate::{
+ insert::NormalBefore,
+ motion,
+ state::{Mode, Operator},
+ PushOperator,
+};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@@ -1332,6 +1337,68 @@ async fn test_find_multibyte(cx: &mut gpui::TestAppContext) {
.assert_eq(r#"<label for="guests">ˇo</label>"#);
}
+#[gpui::test]
+async fn test_sneak(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.update(|cx| {
+ cx.bind_keys([
+ KeyBinding::new(
+ "s",
+ PushOperator(Operator::Sneak { first_char: None }),
+ Some("vim_mode == normal"),
+ ),
+ KeyBinding::new(
+ "S",
+ PushOperator(Operator::SneakBackward { first_char: None }),
+ Some("vim_mode == normal"),
+ ),
+ KeyBinding::new(
+ "S",
+ PushOperator(Operator::SneakBackward { first_char: None }),
+ Some("vim_mode == visual"),
+ ),
+ ])
+ });
+
+ // Sneak forwards multibyte & multiline
+ cx.set_state(
+ indoc! {
+ r#"<labelˇ for="guests">
+ Počet hostů
+ </label>"#
+ },
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("s t ů");
+ cx.assert_state(
+ indoc! {
+ r#"<label for="guests">
+ Počet hosˇtů
+ </label>"#
+ },
+ Mode::Normal,
+ );
+
+ // Visual sneak backwards multibyte & multiline
+ cx.simulate_keystrokes("v S < l");
+ cx.assert_state(
+ indoc! {
+ r#"«ˇ<label for="guests">
+ Počet host»ů
+ </label>"#
+ },
+ Mode::Visual,
+ );
+
+ // Sneak backwards repeated
+ cx.set_state(r#"11 12 13 ˇ14"#, Mode::Normal);
+ cx.simulate_keystrokes("S space 1");
+ cx.assert_state(r#"11 12ˇ 13 14"#, Mode::Normal);
+ cx.simulate_keystrokes(";");
+ cx.assert_state(r#"11ˇ 12 13 14"#, Mode::Normal);
+}
+
#[gpui::test]
async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;