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",
|
||||
"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 } }],
|
||||
|
|
|
@ -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;
|
||||
|
|
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