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:
parent
597e5f8304
commit
57e4540759
4 changed files with 289 additions and 1 deletions
|
@ -55,6 +55,10 @@
|
||||||
"n": "vim::MoveToNextMatch",
|
"n": "vim::MoveToNextMatch",
|
||||||
"shift-n": "vim::MoveToPrevMatch",
|
"shift-n": "vim::MoveToPrevMatch",
|
||||||
"%": "vim::Matching",
|
"%": "vim::Matching",
|
||||||
|
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
|
||||||
|
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
|
||||||
|
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
|
||||||
|
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
|
||||||
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
|
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
|
||||||
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
|
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
|
||||||
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
|
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
|
||||||
|
|
|
@ -72,6 +72,12 @@ pub enum Motion {
|
||||||
StartOfDocument,
|
StartOfDocument,
|
||||||
EndOfDocument,
|
EndOfDocument,
|
||||||
Matching,
|
Matching,
|
||||||
|
UnmatchedForward {
|
||||||
|
char: char,
|
||||||
|
},
|
||||||
|
UnmatchedBackward {
|
||||||
|
char: char,
|
||||||
|
},
|
||||||
FindForward {
|
FindForward {
|
||||||
before: bool,
|
before: bool,
|
||||||
char: char,
|
char: char,
|
||||||
|
@ -203,6 +209,20 @@ pub struct StartOfLine {
|
||||||
pub(crate) display_lines: bool,
|
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!(
|
impl_actions!(
|
||||||
vim,
|
vim,
|
||||||
[
|
[
|
||||||
|
@ -219,6 +239,8 @@ impl_actions!(
|
||||||
NextSubwordEnd,
|
NextSubwordEnd,
|
||||||
PreviousSubwordStart,
|
PreviousSubwordStart,
|
||||||
PreviousSubwordEnd,
|
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::action(editor, cx, |vim, _: &Matching, cx| {
|
||||||
vim.motion(Motion::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(
|
Vim::action(
|
||||||
editor,
|
editor,
|
||||||
cx,
|
cx,
|
||||||
|
@ -504,6 +539,8 @@ impl Motion {
|
||||||
| Jump { line: true, .. } => true,
|
| Jump { line: true, .. } => true,
|
||||||
EndOfLine { .. }
|
EndOfLine { .. }
|
||||||
| Matching
|
| Matching
|
||||||
|
| UnmatchedForward { .. }
|
||||||
|
| UnmatchedBackward { .. }
|
||||||
| FindForward { .. }
|
| FindForward { .. }
|
||||||
| Left
|
| Left
|
||||||
| Backspace
|
| Backspace
|
||||||
|
@ -537,6 +574,8 @@ impl Motion {
|
||||||
| Up { .. }
|
| Up { .. }
|
||||||
| EndOfLine { .. }
|
| EndOfLine { .. }
|
||||||
| Matching
|
| Matching
|
||||||
|
| UnmatchedForward { .. }
|
||||||
|
| UnmatchedBackward { .. }
|
||||||
| FindForward { .. }
|
| FindForward { .. }
|
||||||
| RepeatFind { .. }
|
| RepeatFind { .. }
|
||||||
| Left
|
| Left
|
||||||
|
@ -583,6 +622,8 @@ impl Motion {
|
||||||
| EndOfLine { .. }
|
| EndOfLine { .. }
|
||||||
| EndOfLineDownward
|
| EndOfLineDownward
|
||||||
| Matching
|
| Matching
|
||||||
|
| UnmatchedForward { .. }
|
||||||
|
| UnmatchedBackward { .. }
|
||||||
| FindForward { .. }
|
| FindForward { .. }
|
||||||
| WindowTop
|
| WindowTop
|
||||||
| WindowMiddle
|
| WindowMiddle
|
||||||
|
@ -707,6 +748,14 @@ impl Motion {
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
Matching => (matching(map, point), 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
|
// t f
|
||||||
FindForward {
|
FindForward {
|
||||||
before,
|
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(
|
fn find_forward(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
from: DisplayPoint,
|
from: DisplayPoint,
|
||||||
|
@ -2118,6 +2253,103 @@ mod test {
|
||||||
cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
|
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]
|
#[gpui::test]
|
||||||
async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
|
async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new_html(cx).await;
|
let mut cx = NeovimBackedTestContext::new_html(cx).await;
|
||||||
|
|
24
crates/vim/test_data/test_unmatched_backward.json
Normal file
24
crates/vim/test_data/test_unmatched_backward.json
Normal 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"}}
|
28
crates/vim/test_data/test_unmatched_forward.json
Normal file
28
crates/vim/test_data/test_unmatched_forward.json
Normal 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"}}
|
Loading…
Add table
Add a link
Reference in a new issue