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,
|
mode: FindRange,
|
||||||
smartcase: bool,
|
smartcase: bool,
|
||||||
},
|
},
|
||||||
|
Sneak {
|
||||||
|
first_char: char,
|
||||||
|
second_char: char,
|
||||||
|
smartcase: bool,
|
||||||
|
},
|
||||||
|
SneakBackward {
|
||||||
|
first_char: char,
|
||||||
|
second_char: char,
|
||||||
|
smartcase: bool,
|
||||||
|
},
|
||||||
RepeatFind {
|
RepeatFind {
|
||||||
last_find: Box<Motion>,
|
last_find: Box<Motion>,
|
||||||
},
|
},
|
||||||
|
@ -538,8 +548,10 @@ impl Vim {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
|
pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
|
if let Some(Operator::FindForward { .. })
|
||||||
self.active_operator()
|
| Some(Operator::Sneak { .. })
|
||||||
|
| Some(Operator::SneakBackward { .. })
|
||||||
|
| Some(Operator::FindBackward { .. }) = self.active_operator()
|
||||||
{
|
{
|
||||||
self.pop_operator(cx);
|
self.pop_operator(cx);
|
||||||
}
|
}
|
||||||
|
@ -625,6 +637,8 @@ impl Motion {
|
||||||
| PreviousSubwordEnd { .. }
|
| PreviousSubwordEnd { .. }
|
||||||
| FirstNonWhitespace { .. }
|
| FirstNonWhitespace { .. }
|
||||||
| FindBackward { .. }
|
| FindBackward { .. }
|
||||||
|
| Sneak { .. }
|
||||||
|
| SneakBackward { .. }
|
||||||
| RepeatFind { .. }
|
| RepeatFind { .. }
|
||||||
| RepeatFindReversed { .. }
|
| RepeatFindReversed { .. }
|
||||||
| Jump { line: false, .. }
|
| Jump { line: false, .. }
|
||||||
|
@ -666,6 +680,8 @@ impl Motion {
|
||||||
| PreviousSubwordEnd { .. }
|
| PreviousSubwordEnd { .. }
|
||||||
| FirstNonWhitespace { .. }
|
| FirstNonWhitespace { .. }
|
||||||
| FindBackward { .. }
|
| FindBackward { .. }
|
||||||
|
| Sneak { .. }
|
||||||
|
| SneakBackward { .. }
|
||||||
| RepeatFindReversed { .. }
|
| RepeatFindReversed { .. }
|
||||||
| WindowTop
|
| WindowTop
|
||||||
| WindowMiddle
|
| WindowMiddle
|
||||||
|
@ -727,6 +743,8 @@ impl Motion {
|
||||||
| PreviousSubwordStart { .. }
|
| PreviousSubwordStart { .. }
|
||||||
| FirstNonWhitespace { .. }
|
| FirstNonWhitespace { .. }
|
||||||
| FindBackward { .. }
|
| FindBackward { .. }
|
||||||
|
| Sneak { .. }
|
||||||
|
| SneakBackward { .. }
|
||||||
| Jump { .. }
|
| Jump { .. }
|
||||||
| NextSectionStart
|
| NextSectionStart
|
||||||
| NextSectionEnd
|
| NextSectionEnd
|
||||||
|
@ -862,6 +880,22 @@ impl Motion {
|
||||||
find_backward(map, point, *after, *char, times, *mode, *smartcase),
|
find_backward(map, point, *after, *char, times, *mode, *smartcase),
|
||||||
SelectionGoal::None,
|
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
|
// ; -- repeat the last find done with t, f, T, F
|
||||||
RepeatFind { last_find } => match **last_find {
|
RepeatFind { last_find } => match **last_find {
|
||||||
Motion::FindForward {
|
Motion::FindForward {
|
||||||
|
@ -895,9 +929,44 @@ impl Motion {
|
||||||
|
|
||||||
(new_point, SelectionGoal::None)
|
(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,
|
_ => 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 {
|
RepeatFindReversed { last_find } => match **last_find {
|
||||||
Motion::FindForward {
|
Motion::FindForward {
|
||||||
before,
|
before,
|
||||||
|
@ -930,6 +999,42 @@ impl Motion {
|
||||||
|
|
||||||
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
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,
|
_ => return None,
|
||||||
},
|
},
|
||||||
NextLineStart => (next_line_start(map, point, times), SelectionGoal::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 {
|
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||||
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
|
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
|
||||||
first_non_whitespace(map, false, correct_line)
|
first_non_whitespace(map, false, correct_line)
|
||||||
|
|
|
@ -68,6 +68,8 @@ pub enum Operator {
|
||||||
Object { around: bool },
|
Object { around: bool },
|
||||||
FindForward { before: bool },
|
FindForward { before: bool },
|
||||||
FindBackward { after: bool },
|
FindBackward { after: bool },
|
||||||
|
Sneak { first_char: Option<char> },
|
||||||
|
SneakBackward { first_char: Option<char> },
|
||||||
AddSurrounds { target: Option<SurroundsType> },
|
AddSurrounds { target: Option<SurroundsType> },
|
||||||
ChangeSurrounds { target: Option<Object> },
|
ChangeSurrounds { target: Option<Object> },
|
||||||
DeleteSurrounds,
|
DeleteSurrounds,
|
||||||
|
@ -460,6 +462,8 @@ impl Operator {
|
||||||
Operator::Literal { .. } => "^V",
|
Operator::Literal { .. } => "^V",
|
||||||
Operator::FindForward { before: false } => "f",
|
Operator::FindForward { before: false } => "f",
|
||||||
Operator::FindForward { before: true } => "t",
|
Operator::FindForward { before: true } => "t",
|
||||||
|
Operator::Sneak { .. } => "s",
|
||||||
|
Operator::SneakBackward { .. } => "S",
|
||||||
Operator::FindBackward { after: false } => "F",
|
Operator::FindBackward { after: false } => "F",
|
||||||
Operator::FindBackward { after: true } => "T",
|
Operator::FindBackward { after: true } => "T",
|
||||||
Operator::AddSurrounds { .. } => "ys",
|
Operator::AddSurrounds { .. } => "ys",
|
||||||
|
@ -502,6 +506,8 @@ impl Operator {
|
||||||
| Operator::Mark
|
| Operator::Mark
|
||||||
| Operator::Jump { .. }
|
| Operator::Jump { .. }
|
||||||
| Operator::FindBackward { .. }
|
| Operator::FindBackward { .. }
|
||||||
|
| Operator::Sneak { .. }
|
||||||
|
| Operator::SneakBackward { .. }
|
||||||
| Operator::Register
|
| Operator::Register
|
||||||
| Operator::RecordRegister
|
| Operator::RecordRegister
|
||||||
| Operator::ReplayRegister
|
| Operator::ReplayRegister
|
||||||
|
|
|
@ -17,7 +17,12 @@ use indoc::indoc;
|
||||||
use search::BufferSearchBar;
|
use search::BufferSearchBar;
|
||||||
use workspace::WorkspaceSettings;
|
use workspace::WorkspaceSettings;
|
||||||
|
|
||||||
use crate::{insert::NormalBefore, motion, state::Mode};
|
use crate::{
|
||||||
|
insert::NormalBefore,
|
||||||
|
motion,
|
||||||
|
state::{Mode, Operator},
|
||||||
|
PushOperator,
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
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>"#);
|
.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]
|
#[gpui::test]
|
||||||
async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
|
async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
|
@ -80,9 +80,11 @@ actions!(
|
||||||
InnerObject,
|
InnerObject,
|
||||||
FindForward,
|
FindForward,
|
||||||
FindBackward,
|
FindBackward,
|
||||||
OpenDefaultKeymap,
|
|
||||||
MaximizePane,
|
MaximizePane,
|
||||||
|
OpenDefaultKeymap,
|
||||||
ResetPaneSizes,
|
ResetPaneSizes,
|
||||||
|
Sneak,
|
||||||
|
SneakBackward,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1093,6 +1095,40 @@ impl Vim {
|
||||||
Vim::globals(cx).last_find = Some(find.clone());
|
Vim::globals(cx).last_find = Some(find.clone());
|
||||||
self.motion(find, cx)
|
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 {
|
Some(Operator::Replace) => match self.mode {
|
||||||
Mode::Normal => self.normal_replace(text, cx),
|
Mode::Normal => self.normal_replace(text, cx),
|
||||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
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
|
### 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:
|
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