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:
parent
0d6a549950
commit
0b105ba8b7
5 changed files with 301 additions and 5 deletions
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -502,6 +506,8 @@ impl Operator {
|
|||
| Operator::Mark
|
||||
| Operator::Jump { .. }
|
||||
| Operator::FindBackward { .. }
|
||||
| Operator::Sneak { .. }
|
||||
| Operator::SneakBackward { .. }
|
||||
| Operator::Register
|
||||
| Operator::RecordRegister
|
||||
| Operator::ReplayRegister
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -80,9 +80,11 @@ actions!(
|
|||
InnerObject,
|
||||
FindForward,
|
||||
FindBackward,
|
||||
OpenDefaultKeymap,
|
||||
MaximizePane,
|
||||
OpenDefaultKeymap,
|
||||
ResetPaneSizes,
|
||||
Sneak,
|
||||
SneakBackward,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -1093,6 +1095,40 @@ impl Vim {
|
|||
Vim::globals(cx).last_find = Some(find.clone());
|
||||
self.motion(find, cx)
|
||||
}
|
||||
Some(Operator::Sneak { first_char }) => {
|
||||
if let Some(first_char) = first_char {
|
||||
if let Some(second_char) = text.chars().next() {
|
||||
let sneak = Motion::Sneak {
|
||||
first_char,
|
||||
second_char,
|
||||
smartcase: VimSettings::get_global(cx).use_smartcase_find,
|
||||
};
|
||||
Vim::globals(cx).last_find = Some((&sneak).clone());
|
||||
self.motion(sneak, cx)
|
||||
}
|
||||
} else {
|
||||
let first_char = text.chars().next();
|
||||
self.pop_operator(cx);
|
||||
self.push_operator(Operator::Sneak { first_char }, cx);
|
||||
}
|
||||
}
|
||||
Some(Operator::SneakBackward { first_char }) => {
|
||||
if let Some(first_char) = first_char {
|
||||
if let Some(second_char) = text.chars().next() {
|
||||
let sneak = Motion::SneakBackward {
|
||||
first_char,
|
||||
second_char,
|
||||
smartcase: VimSettings::get_global(cx).use_smartcase_find,
|
||||
};
|
||||
Vim::globals(cx).last_find = Some((&sneak).clone());
|
||||
self.motion(sneak, cx)
|
||||
}
|
||||
} else {
|
||||
let first_char = text.chars().next();
|
||||
self.pop_operator(cx);
|
||||
self.push_operator(Operator::SneakBackward { first_char }, cx);
|
||||
}
|
||||
}
|
||||
Some(Operator::Replace) => match self.mode {
|
||||
Mode::Normal => self.normal_replace(text, cx),
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
||||
|
|
|
@ -408,6 +408,20 @@ Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), b
|
|||
}
|
||||
```
|
||||
|
||||
The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for quick navigation to any two-character sequence in your text. You can enable it by adding the following keybindings to your keymap. By default, the `s` key is mapped to `vim::Substitute`. Adding these bindings will override that behavior, so ensure this change aligns with your workflow preferences.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"context": "vim_mode == normal || vim_mode == visual",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", { "Sneak": {} }],
|
||||
"S": ["vim::PushOperator", { "SneakBackward": {} }]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Restoring common text editing keybindings
|
||||
|
||||
If you're using vim mode on Linux or Windows, you may find it overrides keybindings you can't live without: `ctrl+v` to copy, `ctrl+f` to search, etc. You can restore them by copying this data into your keymap:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue