From d791c6cdb1ccf4e2371fdd869399eeba38ef0705 Mon Sep 17 00:00:00 2001 From: Alex Shen <31595285+x4132@users.noreply.github.com> Date: Fri, 16 May 2025 14:21:30 -0700 Subject: [PATCH] vim: Add `g M` motion to go to the middle of a line (#30227) Adds the "g M" vim motion to go to the middle of the line. --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 1 + crates/vim/src/motion.rs | 111 ++++++++++++++++++ ...orced_motion_delete_to_middle_of_line.json | 34 ++++++ 3 files changed, 146 insertions(+) create mode 100644 crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 91dde540ce..bba5d2d78e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -152,6 +152,7 @@ "g end": ["vim::EndOfLine", { "display_lines": true }], "g 0": ["vim::StartOfLine", { "display_lines": true }], "g home": ["vim::StartOfLine", { "display_lines": true }], + "g shift-m": ["vim::MiddleOfLine", { "display_lines": true }], "g ^": ["vim::FirstNonWhitespace", { "display_lines": true }], "g v": "vim::RestoreVisualSelection", "g ]": "editor::GoToDiagnostic", diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index f582fea166..b207307f2d 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -84,6 +84,9 @@ pub enum Motion { StartOfLine { display_lines: bool, }, + MiddleOfLine { + display_lines: bool, + }, EndOfLine { display_lines: bool, }, @@ -265,6 +268,13 @@ pub struct StartOfLine { pub(crate) display_lines: bool, } +#[derive(Clone, Deserialize, JsonSchema, PartialEq)] +#[serde(deny_unknown_fields)] +struct MiddleOfLine { + #[serde(default)] + display_lines: bool, +} + #[derive(Clone, Deserialize, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] struct UnmatchedForward { @@ -283,6 +293,7 @@ impl_actions!( vim, [ StartOfLine, + MiddleOfLine, EndOfLine, FirstNonWhitespace, Down, @@ -409,6 +420,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { cx, ) }); + Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| { + vim.motion( + Motion::MiddleOfLine { + display_lines: action.display_lines, + }, + window, + cx, + ) + }); Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| { vim.motion( Motion::EndOfLine { @@ -737,6 +757,7 @@ impl Motion { | SentenceBackward | SentenceForward | GoToColumn + | MiddleOfLine { .. } | UnmatchedForward { .. } | UnmatchedBackward { .. } | NextWordStart { .. } @@ -769,6 +790,7 @@ impl Motion { Down { .. } | Up { .. } | EndOfLine { .. } + | MiddleOfLine { .. } | Matching | UnmatchedForward { .. } | UnmatchedBackward { .. } @@ -894,6 +916,10 @@ impl Motion { start_of_line(map, *display_lines, point), SelectionGoal::None, ), + MiddleOfLine { display_lines } => ( + middle_of_line(map, *display_lines, point, maybe_times), + SelectionGoal::None, + ), EndOfLine { display_lines } => ( end_of_line(map, *display_lines, point, times), SelectionGoal::None, @@ -1944,6 +1970,36 @@ pub(crate) fn start_of_line( } } +pub(crate) fn middle_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, + times: Option, +) -> DisplayPoint { + let percent = if let Some(times) = times.filter(|&t| t <= 100) { + times as f64 / 100. + } else { + 0.5 + }; + if display_lines { + map.clip_point( + DisplayPoint::new( + point.row(), + (map.line_len(point.row()) as f64 * percent) as u32, + ), + Bias::Left, + ) + } else { + let mut buffer_point = point.to_point(map); + buffer_point.column = (map + .buffer_snapshot + .line_len(MultiBufferRow(buffer_point.row)) as f64 + * percent) as u32; + + map.clip_point(buffer_point.to_display_point(map), Bias::Left) + } +} + pub(crate) fn end_of_line( map: &DisplaySnapshot, display_lines: bool, @@ -3906,6 +3962,61 @@ mod test { assert_eq!(cx.cx.forced_motion(), false); } + #[gpui::test] + async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v g shift-m").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇbrown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick bˇrown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v g shift-m").await; + cx.shared_state().await.assert_eq(indoc! {" + the quickˇown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v g shift-m").await; + cx.shared_state().await.assert_eq(indoc! {" + the quicˇk + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇ fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇuick brown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + } + #[gpui::test] async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json new file mode 100644 index 0000000000..ca6aa52804 --- /dev/null +++ b/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json @@ -0,0 +1,34 @@ +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"g"} +{"Key":"shift-m"} +{"Get":{"state":"ˇbrown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"g"} +{"Key":"shift-m"} +{"Get":{"state":"the quickˇown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"g"} +{"Key":"shift-m"} +{"Get":{"state":"the quicˇk\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"7"} +{"Key":"5"} +{"Key":"g"} +{"Key":"shift-m"} +{"Get":{"state":"ˇ fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"2"} +{"Key":"3"} +{"Key":"g"} +{"Key":"shift-m"} +{"Get":{"state":"ˇuick brown fox\njumped over the lazy dog","mode":"Normal"}}