Skip to content

Commit

Permalink
vim: Add sneak motion (zed-industries#22793)
Browse files Browse the repository at this point in the history
A (re)continuation of zed-industries#21067. 

This takes the original implementation in
zed-industries#15572 and adds the test in
zed-industries#21067. Then, as requested in
zed-industries#21067 (comment),
it documents how to map a keybinding instead of having a setting.

Closes zed-industries#13858

Release Notes:

- Added support for the popular
[vim_sneak](https://github.com/justinmk/vim-sneak) plugin. This is
disabled by default and can be enabled by binding a key to the `Sneak`
and `SneakBackward` operators.

Reference:
https://github.com/justinmk/vim-sneak

---------

Co-authored-by: Kajetan Puchalski <[email protected]>
Co-authored-by: Aidan Grant <[email protected]>
Co-authored-by: Conrad Irwin <[email protected]>
  • Loading branch information
4 people authored Jan 10, 2025
1 parent 0d6a549 commit 0b105ba
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 5 deletions.
179 changes: 176 additions & 3 deletions crates/vim/src/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
},
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -625,6 +637,8 @@ impl Motion {
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Sneak { .. }
| SneakBackward { .. }
| RepeatFind { .. }
| RepeatFindReversed { .. }
| Jump { line: false, .. }
Expand Down Expand Up @@ -666,6 +680,8 @@ impl Motion {
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Sneak { .. }
| SneakBackward { .. }
| RepeatFindReversed { .. }
| WindowTop
| WindowMiddle
Expand Down Expand Up @@ -727,6 +743,8 @@ impl Motion {
| PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Sneak { .. }
| SneakBackward { .. }
| Jump { .. }
| NextSectionStart
| NextSectionEnd
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions crates/vim/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ pub enum Operator {
Object { around: bool },
FindForward { before: bool },
FindBackward { after: bool },
Sneak { first_char: Option<char> },
SneakBackward { first_char: Option<char> },
AddSurrounds { target: Option<SurroundsType> },
ChangeSurrounds { target: Option<Object> },
DeleteSurrounds,
Expand Down Expand Up @@ -460,6 +462,8 @@ impl Operator {
Operator::Literal { .. } => "^V",
Operator::FindForward { before: false } => "f",
Operator::FindForward { before: true } => "t",
Operator::Sneak { .. } => "s",
Operator::SneakBackward { .. } => "S",
Operator::FindBackward { after: false } => "F",
Operator::FindBackward { after: true } => "T",
Operator::AddSurrounds { .. } => "ys",
Expand Down Expand Up @@ -502,6 +506,8 @@ impl Operator {
| Operator::Mark
| Operator::Jump { .. }
| Operator::FindBackward { .. }
| Operator::Sneak { .. }
| Operator::SneakBackward { .. }
| Operator::Register
| Operator::RecordRegister
| Operator::ReplayRegister
Expand Down
69 changes: 68 additions & 1 deletion crates/vim/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0b105ba

Please sign in to comment.