Attempt to fix `go to the end of the line` when using helix mode (#41575)

HuaGu-Dragon and Jakub Konka created

Closes #41550

Release Notes:

- Fixed `<g-l>` behavior in helix mode which will now correctly go to the last charactor of the line.
- Fixed not switching to helix normal mode when in default vim context and pressing escape.

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>

Change summary

assets/keymaps/vim.json |  3 +
crates/vim/src/helix.rs | 80 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 82 insertions(+), 1 deletion(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -414,8 +414,9 @@
     }
   },
   {
-    "context": "vim_mode == helix_normal && !menu",
+    "context": "VimControl && vim_mode == helix_normal && !menu",
     "bindings": {
+      "escape": "vim::SwitchToHelixNormalMode",
       "i": "vim::HelixInsert",
       "a": "vim::HelixAppend",
       "ctrl-[": "editor::Cancel"

crates/vim/src/helix.rs 🔗

@@ -263,6 +263,31 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         match motion {
+            Motion::EndOfLine { .. } => {
+                // In Helix mode, EndOfLine should position cursor ON the last character,
+                // not after it. We therefore need special handling for it.
+                self.update_editor(cx, |_, editor, cx| {
+                    let text_layout_details = editor.text_layout_details(window);
+                    editor.change_selections(Default::default(), window, cx, |s| {
+                        s.move_with(|map, selection| {
+                            let goal = selection.goal;
+                            let cursor = if selection.is_empty() || selection.reversed {
+                                selection.head()
+                            } else {
+                                movement::left(map, selection.head())
+                            };
+
+                            let (point, _goal) = motion
+                                .move_point(map, cursor, goal, times, &text_layout_details)
+                                .unwrap_or((cursor, goal));
+
+                            // Move left by one character to position on the last character
+                            let adjusted_point = movement::saturating_left(map, point);
+                            selection.collapse_to(adjusted_point, SelectionGoal::None)
+                        })
+                    });
+                });
+            }
             Motion::NextWordStart { ignore_punctuation } => {
                 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
                     let left_kind = classifier.kind_with(left, ignore_punctuation);
@@ -1493,4 +1518,59 @@ mod test {
             Mode::Insert,
         );
     }
+
+    #[gpui::test]
+    async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+
+        // Test g l moves to last character, not after it
+        cx.set_state("hello ˇworld!", Mode::HelixNormal);
+        cx.simulate_keystrokes("g l");
+        cx.assert_state("hello worldˇ!", Mode::HelixNormal);
+
+        // Test with Chinese characters, test if work with UTF-8?
+        cx.set_state("ˇ你好世界", Mode::HelixNormal);
+        cx.simulate_keystrokes("g l");
+        cx.assert_state("你好世ˇ界", Mode::HelixNormal);
+
+        // Test with end of line
+        cx.set_state("endˇ", Mode::HelixNormal);
+        cx.simulate_keystrokes("g l");
+        cx.assert_state("enˇd", Mode::HelixNormal);
+
+        // Test with empty line
+        cx.set_state(
+            indoc! {"
+                hello
+                ˇ
+                world"},
+            Mode::HelixNormal,
+        );
+        cx.simulate_keystrokes("g l");
+        cx.assert_state(
+            indoc! {"
+                hello
+                ˇ
+                world"},
+            Mode::HelixNormal,
+        );
+
+        // Test with multiple lines
+        cx.set_state(
+            indoc! {"
+                ˇfirst line
+                second line
+                third line"},
+            Mode::HelixNormal,
+        );
+        cx.simulate_keystrokes("g l");
+        cx.assert_state(
+            indoc! {"
+                first linˇe
+                second line
+                third line"},
+            Mode::HelixNormal,
+        );
+    }
 }