vim: Implement <count>% motion (#25839)

Closes https://github.com/zed-industries/zed/discussions/25665

> Currently Zed is missing quite an useful Vim motion: <count>% (go to
{count} percentage in the file).
Description:
{count}% - Go to {count} percentage in the file, on the first non-blank
in the line linewise. To compute the new line number this formula is
used: ({count} * number-of-lines + 99) / 100 .
> [Link](https://neovim.io/doc/user/motion.html#N%25).

Release Notes:

- vim: Added `<count>%` motion

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
This commit is contained in:
brian tan 2025-03-05 21:59:18 -05:00 committed by GitHub
parent 314ad5dd5f
commit 22b1a02e23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 1 deletions

View file

@ -247,7 +247,8 @@
"context": "VimControl && VimCount",
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
":": "vim::CountCommand",
"%": "vim::GoToPercentage"
}
},
{

View file

@ -74,6 +74,7 @@ pub enum Motion {
StartOfDocument,
EndOfDocument,
Matching,
GoToPercentage,
UnmatchedForward {
char: char,
},
@ -281,6 +282,7 @@ actions!(
StartOfDocument,
EndOfDocument,
Matching,
GoToPercentage,
NextLineStart,
PreviousLineStart,
StartOfLineDownward,
@ -402,6 +404,9 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
vim.motion(Motion::Matching, window, cx)
});
Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
vim.motion(Motion::GoToPercentage, window, cx)
});
Vim::action(
editor,
cx,
@ -643,6 +648,7 @@ impl Motion {
| PreviousMethodEnd
| NextComment
| PreviousComment
| GoToPercentage
| Jump { line: true, .. } => true,
EndOfLine { .. }
| Matching
@ -701,6 +707,7 @@ impl Motion {
| StartOfLineDownward
| EndOfLineDownward
| GoToColumn
| GoToPercentage
| NextWordStart { .. }
| NextWordEnd { .. }
| PreviousWordStart { .. }
@ -745,6 +752,7 @@ impl Motion {
| EndOfLine { .. }
| EndOfLineDownward
| Matching
| GoToPercentage
| UnmatchedForward { .. }
| UnmatchedBackward { .. }
| FindForward { .. }
@ -886,6 +894,7 @@ impl Motion {
SelectionGoal::None,
),
Matching => (matching(map, point), SelectionGoal::None),
GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
UnmatchedForward { char } => (
unmatched_forward(map, point, *char, times),
SelectionGoal::None,
@ -2194,6 +2203,22 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
}
}
// Go to {count} percentage in the file, on the first
// non-blank in the line linewise. To compute the new
// line number this formula is used:
// ({count} * number-of-lines + 99) / 100
//
// https://neovim.io/doc/user/motion.html#N%25
fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
let total_lines = map.buffer_snapshot.max_point().row + 1;
let target_line = (count * total_lines as usize + 99) / 100;
let target_point = DisplayPoint::new(
DisplayRow(target_line.saturating_sub(1) as u32),
point.column(),
);
map.clip_point(target_point, Bias::Left)
}
fn unmatched_forward(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
@ -3470,4 +3495,103 @@ mod test {
Mode::Normal,
);
}
#[gpui::test]
async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// Normal mode
cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes("2 0 %").await;
cx.shared_state().await.assert_eq(indoc! {"
The quick brown
fox ˇjumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"});
cx.simulate_shared_keystrokes("2 5 %").await;
cx.shared_state().await.assert_eq(indoc! {"
The quick brown
fox jumps over
the ˇlazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"});
cx.simulate_shared_keystrokes("7 5 %").await;
cx.shared_state().await.assert_eq(indoc! {"
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The ˇquick brown
fox jumps over
the lazy dog"});
// Visual mode
cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes("v 5 0 %").await;
cx.shared_state().await.assert_eq(indoc! {"
The «quick brown
fox jumps over
the lazy dog
The quick brown
fox »umps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"});
cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes("v 1 0 0 %").await;
cx.shared_state().await.assert_eq(indoc! {"
The «quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the lazy dog
The quick brown
fox jumps over
the »azy dog"});
}
}

View file

@ -0,0 +1,26 @@
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"2"}
{"Key":"0"}
{"Key":"%"}
{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"2"}
{"Key":"5"}
{"Key":"%"}
{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"7"}
{"Key":"5"}
{"Key":"%"}
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"5"}
{"Key":"0"}
{"Key":"%"}
{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jˇ»umps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}}
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"1"}
{"Key":"0"}
{"Key":"0"}
{"Key":"%"}
{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lˇ»azy dog","mode":"Visual"}}