vim: Add "unmatched" motions ]}, ]), [{ and [( (#21098)

Closes #20791

Release Notes:

- Added vim ["unmatched"
motions](1d87e11a1e/runtime/doc/motion.txt (L1238-L1255))
`]}`, `])`, `[{` and `[(`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Helge Mahrt 2024-11-26 23:08:54 +01:00 committed by GitHub
parent 597e5f8304
commit 57e4540759
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 289 additions and 1 deletions

View file

@ -55,6 +55,10 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],

View file

@ -72,6 +72,12 @@ pub enum Motion {
StartOfDocument,
EndOfDocument,
Matching,
UnmatchedForward {
char: char,
},
UnmatchedBackward {
char: char,
},
FindForward {
before: bool,
char: char,
@ -203,6 +209,20 @@ pub struct StartOfLine {
pub(crate) display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UnmatchedForward {
#[serde(default)]
char: char,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UnmatchedBackward {
#[serde(default)]
char: char,
}
impl_actions!(
vim,
[
@ -219,6 +239,8 @@ impl_actions!(
NextSubwordEnd,
PreviousSubwordStart,
PreviousSubwordEnd,
UnmatchedForward,
UnmatchedBackward
]
);
@ -326,7 +348,20 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Matching, cx| {
vim.motion(Motion::Matching, cx)
});
Vim::action(
editor,
cx,
|vim, &UnmatchedForward { char }: &UnmatchedForward, cx| {
vim.motion(Motion::UnmatchedForward { char }, cx)
},
);
Vim::action(
editor,
cx,
|vim, &UnmatchedBackward { char }: &UnmatchedBackward, cx| {
vim.motion(Motion::UnmatchedBackward { char }, cx)
},
);
Vim::action(
editor,
cx,
@ -504,6 +539,8 @@ impl Motion {
| Jump { line: true, .. } => true,
EndOfLine { .. }
| Matching
| UnmatchedForward { .. }
| UnmatchedBackward { .. }
| FindForward { .. }
| Left
| Backspace
@ -537,6 +574,8 @@ impl Motion {
| Up { .. }
| EndOfLine { .. }
| Matching
| UnmatchedForward { .. }
| UnmatchedBackward { .. }
| FindForward { .. }
| RepeatFind { .. }
| Left
@ -583,6 +622,8 @@ impl Motion {
| EndOfLine { .. }
| EndOfLineDownward
| Matching
| UnmatchedForward { .. }
| UnmatchedBackward { .. }
| FindForward { .. }
| WindowTop
| WindowMiddle
@ -707,6 +748,14 @@ impl Motion {
SelectionGoal::None,
),
Matching => (matching(map, point), SelectionGoal::None),
UnmatchedForward { char } => (
unmatched_forward(map, point, *char, times),
SelectionGoal::None,
),
UnmatchedBackward { char } => (
unmatched_backward(map, point, *char, times),
SelectionGoal::None,
),
// t f
FindForward {
before,
@ -1792,6 +1841,92 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
}
}
fn unmatched_forward(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
char: char,
times: usize,
) -> DisplayPoint {
for _ in 0..times {
// https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
let point = display_point.to_point(map);
let offset = point.to_offset(&map.buffer_snapshot);
let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
let Some(ranges) = ranges else { break };
let mut closest_closing_destination = None;
let mut closest_distance = usize::MAX;
for (_, close_range) in ranges {
if close_range.start > offset {
let mut chars = map.buffer_snapshot.chars_at(close_range.start);
if Some(char) == chars.next() {
let distance = close_range.start - offset;
if distance < closest_distance {
closest_closing_destination = Some(close_range.start);
closest_distance = distance;
continue;
}
}
}
}
let new_point = closest_closing_destination
.map(|destination| destination.to_display_point(map))
.unwrap_or(display_point);
if new_point == display_point {
break;
}
display_point = new_point;
}
return display_point;
}
fn unmatched_backward(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
char: char,
times: usize,
) -> DisplayPoint {
for _ in 0..times {
// https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
let point = display_point.to_point(map);
let offset = point.to_offset(&map.buffer_snapshot);
let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
let Some(ranges) = ranges else {
break;
};
let mut closest_starting_destination = None;
let mut closest_distance = usize::MAX;
for (start_range, _) in ranges {
if start_range.start < offset {
let mut chars = map.buffer_snapshot.chars_at(start_range.start);
if Some(char) == chars.next() {
let distance = offset - start_range.start;
if distance < closest_distance {
closest_starting_destination = Some(start_range.start);
closest_distance = distance;
continue;
}
}
}
}
let new_point = closest_starting_destination
.map(|destination| destination.to_display_point(map))
.unwrap_or(display_point);
if new_point == display_point {
break;
} else {
display_point = new_point;
}
}
display_point
}
fn find_forward(
map: &DisplaySnapshot,
from: DisplayPoint,
@ -2118,6 +2253,103 @@ mod test {
cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
}
#[gpui::test]
async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// test it works with curly braces
cx.set_shared_state(indoc! {r"func (a string) {
do(something(with<Types>.anˇd_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes("] }").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"func (a string) {
do(something(with<Types>.and_arrays[0, 2]))
ˇ}"});
// test it works with brackets
cx.set_shared_state(indoc! {r"func (a string) {
do(somethiˇng(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes("] )").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"func (a string) {
do(something(with<Types>.and_arrays[0, 2])ˇ)
}"});
cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
.await;
cx.simulate_shared_keystrokes("] )").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
// test it works on immediate nesting
cx.set_shared_state("{ˇ {}{}}").await;
cx.simulate_shared_keystrokes("] }").await;
cx.shared_state().await.assert_eq("{ {}{}ˇ}");
cx.set_shared_state("(ˇ ()())").await;
cx.simulate_shared_keystrokes("] )").await;
cx.shared_state().await.assert_eq("( ()()ˇ)");
// test it works on immediate nesting inside braces
cx.set_shared_state("{\n ˇ {()}\n}").await;
cx.simulate_shared_keystrokes("] }").await;
cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
cx.set_shared_state("(\n ˇ {()}\n)").await;
cx.simulate_shared_keystrokes("] )").await;
cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
}
#[gpui::test]
async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// test it works with curly braces
cx.set_shared_state(indoc! {r"func (a string) {
do(something(with<Types>.anˇd_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes("[ {").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"func (a string) ˇ{
do(something(with<Types>.and_arrays[0, 2]))
}"});
// test it works with brackets
cx.set_shared_state(indoc! {r"func (a string) {
do(somethiˇng(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes("[ (").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"func (a string) {
doˇ(something(with<Types>.and_arrays[0, 2]))
}"});
// test it works on immediate nesting
cx.set_shared_state("{{}{} ˇ }").await;
cx.simulate_shared_keystrokes("[ {").await;
cx.shared_state().await.assert_eq("ˇ{{}{} }");
cx.set_shared_state("(()() ˇ )").await;
cx.simulate_shared_keystrokes("[ (").await;
cx.shared_state().await.assert_eq("ˇ(()() )");
// test it works on immediate nesting inside braces
cx.set_shared_state("{\n {()} ˇ\n}").await;
cx.simulate_shared_keystrokes("[ {").await;
cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
cx.set_shared_state("(\n {()} ˇ\n)").await;
cx.simulate_shared_keystrokes("[ (").await;
cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
}
#[gpui::test]
async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new_html(cx).await;

View file

@ -0,0 +1,24 @@
{"Put":{"state":"func (a string) {\n do(something(with<Types>.anˇd_arrays[0, 2]))\n}"}}
{"Key":"["}
{"Key":"{"}
{"Get":{"state":"func (a string) ˇ{\n do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
{"Put":{"state":"func (a string) {\n do(somethiˇng(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"["}
{"Key":"("}
{"Get":{"state":"func (a string) {\n doˇ(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
{"Put":{"state":"{{}{} ˇ }"}}
{"Key":"["}
{"Key":"{"}
{"Get":{"state":"ˇ{{}{} }","mode":"Normal"}}
{"Put":{"state":"(()() ˇ )"}}
{"Key":"["}
{"Key":"("}
{"Get":{"state":"ˇ(()() )","mode":"Normal"}}
{"Put":{"state":"{\n {()} ˇ\n}"}}
{"Key":"["}
{"Key":"{"}
{"Get":{"state":"ˇ{\n {()} \n}","mode":"Normal"}}
{"Put":{"state":"(\n {()} ˇ\n)"}}
{"Key":"["}
{"Key":"("}
{"Get":{"state":"ˇ(\n {()} \n)","mode":"Normal"}}

View file

@ -0,0 +1,28 @@
{"Put":{"state":"func (a string) {\n do(something(with<Types>.anˇd_arrays[0, 2]))\n}"}}
{"Key":"]"}
{"Key":"}"}
{"Get":{"state":"func (a string) {\n do(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
{"Put":{"state":"func (a string) {\n do(somethiˇng(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"]"}
{"Key":")"}
{"Get":{"state":"func (a string) {\n do(something(with<Types>.and_arrays[0, 2])ˇ)\n}","mode":"Normal"}}
{"Put":{"state":"func (a string) { a((b, cˇ))}"}}
{"Key":"]"}
{"Key":")"}
{"Get":{"state":"func (a string) { a((b, c)ˇ)}","mode":"Normal"}}
{"Put":{"state":"{ˇ {}{}}"}}
{"Key":"]"}
{"Key":"}"}
{"Get":{"state":"{ {}{}ˇ}","mode":"Normal"}}
{"Put":{"state":"(ˇ ()())"}}
{"Key":"]"}
{"Key":")"}
{"Get":{"state":"( ()()ˇ)","mode":"Normal"}}
{"Put":{"state":"{\n ˇ {()}\n}"}}
{"Key":"]"}
{"Key":"}"}
{"Get":{"state":"{\n {()}\nˇ}","mode":"Normal"}}
{"Put":{"state":"(\n ˇ {()}\n)"}}
{"Key":"]"}
{"Key":")"}
{"Get":{"state":"(\n {()}\nˇ)","mode":"Normal"}}