mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-24 06:19:37 +00:00
vim: Add support for moving to first, middle and last visible lines (H
, L
, M
) (#6919)
This change implements the vim [motion](https://github.com/vim/vim/blob/master/runtime/doc/motion.txt) commands to move the cursor to the top, middle and bottom of the visible view. This feature is requested in https://github.com/zed-industries/zed/issues/4941. This change takes inspiration from [crates/vim/src/normal/scroll.rs](https://github.com/zed-industries/zed/blob/main/crates/vim/src/normal/scroll.rs). A note on the behavior of these commands: Because `NeovimBackedTestContext` requires compatibility with nvim, the current implementation causes slightly non-standard behavior: it causes the editor to scroll a few lines. The standard behavior causes no scrolling. It is easy enough to account for the margin by adding `VERTICAL_SCROLL_MARGIN`. However, doing so will cause test failures due to the disparity between nvim and zed states. Perhaps `NeovimBackedTestContext` should have a switch to be more tolerant for such cases. Release Notes: - Added support for moving to top, middle and bottom of the screen in vim mode (`H`, `M`, and `L`) ([#4941](https://github.com/zed-industries/zed/issues/4941)).
This commit is contained in:
parent
1ab49fdbe6
commit
31e9526544
9 changed files with 337 additions and 1 deletions
|
@ -341,6 +341,9 @@
|
|||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "editor::Indent",
|
||||
"< <": "editor::Outdent",
|
||||
"shift-h": "vim::WindowTop",
|
||||
"shift-m": "vim::WindowMiddle",
|
||||
"shift-l": "vim::WindowBottom",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem"
|
||||
}
|
||||
|
|
|
@ -586,6 +586,8 @@ impl DisplaySnapshot {
|
|||
text_system,
|
||||
editor_style,
|
||||
rem_size,
|
||||
anchor: _,
|
||||
visible_rows: _,
|
||||
}: &TextLayoutDetails,
|
||||
) -> Arc<LineLayout> {
|
||||
let mut runs = Vec::new();
|
||||
|
|
|
@ -3052,6 +3052,8 @@ impl Editor {
|
|||
text_system: cx.text_system().clone(),
|
||||
editor_style: self.style.clone().unwrap(),
|
||||
rem_size: cx.rem_size(),
|
||||
anchor: self.scroll_manager.anchor().anchor,
|
||||
visible_rows: self.visible_line_count(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ use language::Point;
|
|||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use multi_buffer::Anchor;
|
||||
|
||||
/// Defines search strategy for items in `movement` module.
|
||||
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
|
||||
/// `FindRange::MultiLine` keeps going until the end of a string.
|
||||
|
@ -23,6 +25,8 @@ pub struct TextLayoutDetails {
|
|||
pub(crate) text_system: Arc<TextSystem>,
|
||||
pub(crate) editor_style: EditorStyle,
|
||||
pub(crate) rem_size: Pixels,
|
||||
pub anchor: Anchor,
|
||||
pub visible_rows: Option<f32>,
|
||||
}
|
||||
|
||||
/// Returns a column to the left of the current point, wrapping
|
||||
|
|
|
@ -299,7 +299,7 @@ impl ScrollManager {
|
|||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn vertical_scroll_margin(&mut self) -> usize {
|
||||
pub fn vertical_scroll_margin(&self) -> usize {
|
||||
self.scroll_manager.vertical_scroll_margin as usize
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,9 @@ pub enum Motion {
|
|||
StartOfLineDownward,
|
||||
EndOfLineDownward,
|
||||
GoToColumn,
|
||||
WindowTop,
|
||||
WindowMiddle,
|
||||
WindowBottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
|
@ -136,6 +139,9 @@ actions!(
|
|||
StartOfLineDownward,
|
||||
EndOfLineDownward,
|
||||
GoToColumn,
|
||||
WindowTop,
|
||||
WindowMiddle,
|
||||
WindowBottom,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -231,6 +237,13 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
|||
workspace.register_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||
repeat_motion(action.backwards, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
|
||||
workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
|
||||
motion(Motion::WindowMiddle, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
|
||||
motion(Motion::WindowBottom, cx)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||
|
@ -295,6 +308,9 @@ impl Motion {
|
|||
| NextLineStart
|
||||
| StartOfLineDownward
|
||||
| StartOfParagraph
|
||||
| WindowTop
|
||||
| WindowMiddle
|
||||
| WindowBottom
|
||||
| EndOfParagraph => true,
|
||||
EndOfLine { .. }
|
||||
| NextWordEnd { .. }
|
||||
|
@ -336,6 +352,9 @@ impl Motion {
|
|||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace { .. }
|
||||
| FindBackward { .. }
|
||||
| WindowTop
|
||||
| WindowMiddle
|
||||
| WindowBottom
|
||||
| NextLineStart => false,
|
||||
}
|
||||
}
|
||||
|
@ -353,6 +372,9 @@ impl Motion {
|
|||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
| WindowTop
|
||||
| WindowMiddle
|
||||
| WindowBottom
|
||||
| NextLineStart => true,
|
||||
Left
|
||||
| Backspace
|
||||
|
@ -449,6 +471,9 @@ impl Motion {
|
|||
StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
|
||||
EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
|
||||
GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
|
||||
WindowTop => window_top(map, point, &text_layout_details),
|
||||
WindowMiddle => window_middle(map, point, &text_layout_details),
|
||||
WindowBottom => window_bottom(map, point, &text_layout_details),
|
||||
};
|
||||
|
||||
(new_point != point || infallible).then_some((new_point, goal))
|
||||
|
@ -955,6 +980,51 @@ pub(crate) fn next_line_end(
|
|||
end_of_line(map, false, point)
|
||||
}
|
||||
|
||||
fn window_top(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let first_visible_line = text_layout_details.anchor.to_display_point(map);
|
||||
let new_col = point.column().min(map.line_len(first_visible_line.row()));
|
||||
let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
|
||||
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
||||
}
|
||||
|
||||
fn window_middle(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
if let Some(visible_rows) = text_layout_details.visible_rows {
|
||||
let first_visible_line = text_layout_details.anchor.to_display_point(map);
|
||||
let max_rows = (visible_rows as u32).min(map.max_buffer_row());
|
||||
let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
|
||||
let new_col = point.column().min(map.line_len(new_row));
|
||||
let new_point = DisplayPoint::new(new_row, new_col);
|
||||
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
||||
} else {
|
||||
(point, SelectionGoal::None)
|
||||
}
|
||||
}
|
||||
|
||||
fn window_bottom(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
if let Some(visible_rows) = text_layout_details.visible_rows {
|
||||
let first_visible_line = text_layout_details.anchor.to_display_point(map);
|
||||
let bottom_row = first_visible_line.row() + (visible_rows) as u32;
|
||||
let bottom_row_capped = bottom_row.min(map.max_buffer_row());
|
||||
let new_col = point.column().min(map.line_len(bottom_row_capped));
|
||||
let new_point = DisplayPoint::new(bottom_row_capped, new_col);
|
||||
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
||||
} else {
|
||||
(point, SelectionGoal::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
@ -1107,4 +1177,218 @@ mod test {
|
|||
cx.simulate_shared_keystrokes(["enter"]).await;
|
||||
cx.assert_shared_state("one\n ˇtwo\nthree").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_window_top(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
let initial_state = indoc! {r"abc
|
||||
def
|
||||
paragraph
|
||||
the second
|
||||
third ˇand
|
||||
final"};
|
||||
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["shift-h"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abˇc
|
||||
def
|
||||
paragraph
|
||||
the second
|
||||
third and
|
||||
final"})
|
||||
.await;
|
||||
|
||||
// clip point
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 ˇ9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-h"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 ˇ3
|
||||
4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
ˇ7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-h"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
ˇ1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_window_middle(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
let initial_state = indoc! {r"abˇc
|
||||
def
|
||||
paragraph
|
||||
the second
|
||||
third and
|
||||
final"};
|
||||
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
paˇragraph
|
||||
the second
|
||||
third and
|
||||
final"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 ˇ9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 ˇ6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
ˇ7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
ˇ4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.set_shared_state(indoc! {r"
|
||||
ˇ1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
ˇ4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
ˇ4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
ˇ4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 ˇ6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-m"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 ˇ6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
let initial_state = indoc! {r"abc
|
||||
deˇf
|
||||
paragraph
|
||||
the second
|
||||
third and
|
||||
final"};
|
||||
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["shift-l"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
paragraph
|
||||
the second
|
||||
third and
|
||||
fiˇnal"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 ˇ6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-l"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
ˇ"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
ˇ4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-l"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
ˇ"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
1 2 ˇ3
|
||||
4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-l"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
ˇ"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"
|
||||
ˇ1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-l"]).await;
|
||||
cx.assert_shared_state(indoc! {r"
|
||||
1 2 3
|
||||
4 5 6
|
||||
7 8 9
|
||||
ˇ"})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
|
15
crates/vim/test_data/test_window_bottom.json
Normal file
15
crates/vim/test_data/test_window_bottom.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{"Put":{"state":"abc\ndeˇf\nparagraph\nthe second\nthird and\nfinal"}}
|
||||
{"Key":"shift-l"}
|
||||
{"Get":{"state":"abc\ndef\nparagraph\nthe second\nthird and\nfiˇnal","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}}
|
||||
{"Key":"shift-l"}
|
||||
{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n"}}
|
||||
{"Key":"shift-l"}
|
||||
{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n"}}
|
||||
{"Key":"shift-l"}
|
||||
{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}}
|
||||
{"Key":"shift-l"}
|
||||
{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}}
|
17
crates/vim/test_data/test_window_middle.json
Normal file
17
crates/vim/test_data/test_window_middle.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{"Put":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"abc\ndef\npaˇragraph\nthe second\nthird and\nfinal","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}}
|
||||
{"Key":"shift-m"}
|
||||
{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}}
|
9
crates/vim/test_data/test_window_top.json
Normal file
9
crates/vim/test_data/test_window_top.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{"Put":{"state":"abc\ndef\nparagraph\nthe second\nthird ˇand\nfinal"}}
|
||||
{"Key":"shift-h"}
|
||||
{"Get":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}}
|
||||
{"Key":"shift-h"}
|
||||
{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}}
|
||||
{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}}
|
||||
{"Key":"shift-h"}
|
||||
{"Get":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n","mode":"Normal"}}
|
Loading…
Reference in a new issue