vim: Maintain end-of-line intent after $ with vertical motions (#45375)

Mayank Verma and dino created

Closes #45340

Release Notes:

- Fixed $ motion in vim mode to stay at end of the line when moving
vertically

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>

Change summary

crates/vim/src/motion.rs                                        | 58 ++
crates/vim/test_data/test_end_of_line_with_vertical_motion.json | 16 
2 files changed, 72 insertions(+), 2 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -10,7 +10,7 @@ use language::{CharKind, Point, Selection, SelectionGoal};
 use multi_buffer::MultiBufferRow;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use std::ops::Range;
+use std::{f64, ops::Range};
 use workspace::searchable::Direction;
 
 use crate::{
@@ -1018,7 +1018,7 @@ impl Motion {
             ),
             EndOfLine { display_lines } => (
                 end_of_line(map, *display_lines, point, times),
-                SelectionGoal::None,
+                SelectionGoal::HorizontalPosition(f64::INFINITY),
             ),
             SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
             SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
@@ -3567,6 +3567,60 @@ mod test {
         cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
     }
 
+    #[gpui::test]
+    async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // test $ followed by k maintains end-of-line position
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fˇox
+            jumps over the
+            lazy dog
+            "})
+            .await;
+        cx.simulate_shared_keystrokes("$ k").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick browˇn
+            fox
+            jumps over the
+            lazy dog
+            "});
+        cx.simulate_shared_keystrokes("j j").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick brown
+            fox
+            jumps over thˇe
+            lazy dog
+            "});
+
+        // test horizontal movement resets the end-of-line behavior
+        cx.set_shared_state(indoc! {"
+            The quick brown fox
+            jumps over the
+            lazy ˇdog
+            "})
+            .await;
+        cx.simulate_shared_keystrokes("$ k").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick brown fox
+            jumps over thˇe
+            lazy dog
+            "});
+        cx.simulate_shared_keystrokes("b b").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick brown fox
+            jumps ˇover the
+            lazy dog
+            "});
+        cx.simulate_shared_keystrokes("k").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quˇick brown fox
+            jumps over the
+            lazy dog
+            "});
+    }
+
     #[gpui::test]
     async fn test_window_top(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/test_data/test_end_of_line_with_vertical_motion.json 🔗

@@ -0,0 +1,16 @@
+{"Put":{"state":"The quick brown\nfˇox\njumps over the\nlazy dog\n"}}
+{"Key":"$"}
+{"Key":"k"}
+{"Get":{"state":"The quick browˇn\nfox\njumps over the\nlazy dog\n","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox\njumps over thˇe\nlazy dog\n","mode":"Normal"}}
+{"Put":{"state":"The quick brown fox\njumps over the\nlazy ˇdog\n"}}
+{"Key":"$"}
+{"Key":"k"}
+{"Get":{"state":"The quick brown fox\njumps over thˇe\nlazy dog\n","mode":"Normal"}}
+{"Key":"b"}
+{"Key":"b"}
+{"Get":{"state":"The quick brown fox\njumps ˇover the\nlazy dog\n","mode":"Normal"}}
+{"Key":"k"}
+{"Get":{"state":"The quˇick brown fox\njumps over the\nlazy dog\n","mode":"Normal"}}