vim: Add sneak motion (#22793)

A (re)continuation of https://github.com/zed-industries/zed/pull/21067. 

This takes the original implementation in
https://github.com/zed-industries/zed/pull/15572 and adds the test in
https://github.com/zed-industries/zed/pull/21067. Then, as requested in
https://github.com/zed-industries/zed/pull/21067#issuecomment-2515469185,
it documents how to map a keybinding instead of having a setting.

Closes #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 <kajetan.puchalski@tuta.io>
Co-authored-by: Aidan Grant <mraidangrant@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Nico Lehmann 2025-01-10 04:07:32 -03:00 committed by GitHub
parent 0d6a549950
commit 0b105ba8b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 301 additions and 5 deletions

View file

@ -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)