Fix panic with Helix mode changing case (#34016)

Richard Feldman created

Closes #33750

Release Notes:

- Fixed a panic when trying to change case in Helix mode

Change summary

crates/vim/src/normal/convert.rs | 41 ++++++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 2 deletions(-)

Detailed changes

crates/vim/src/normal/convert.rs 🔗

@@ -212,7 +212,19 @@ impl Vim {
                         }
                     }
 
-                    Mode::HelixNormal => {}
+                    Mode::HelixNormal => {
+                        if selection.is_empty() {
+                            // Handle empty selection by operating on the whole word
+                            let (word_range, _) = snapshot.surrounding_word(selection.start, false);
+                            let word_start = snapshot.offset_to_point(word_range.start);
+                            let word_end = snapshot.offset_to_point(word_range.end);
+                            ranges.push(word_start..word_end);
+                            cursor_positions.push(selection.start..selection.start);
+                        } else {
+                            ranges.push(selection.start..selection.end);
+                            cursor_positions.push(selection.start..selection.end);
+                        }
+                    }
                     Mode::Insert | Mode::Normal | Mode::Replace => {
                         let start = selection.start;
                         let mut end = start;
@@ -245,12 +257,16 @@ impl Vim {
                 })
             });
         });
-        self.switch_mode(Mode::Normal, true, window, cx)
+        if self.mode != Mode::HelixNormal {
+            self.switch_mode(Mode::Normal, true, window, cx)
+        }
     }
 }
 
 #[cfg(test)]
 mod test {
+    use crate::test::VimTestContext;
+
     use crate::{state::Mode, test::NeovimBackedTestContext};
 
     #[gpui::test]
@@ -419,4 +435,25 @@ mod test {
             .await
             .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM");
     }
+
+    #[gpui::test]
+    async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Explicit selection
+        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
+        cx.simulate_keystrokes("~");
+        cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal);
+
+        // Cursor-only (empty) selection
+        cx.set_state("The ˇquick brown", Mode::HelixNormal);
+        cx.simulate_keystrokes("~");
+        cx.assert_state("The ˇQUICK brown", Mode::HelixNormal);
+
+        // With `e` motion (which extends selection to end of word in Helix)
+        cx.set_state("The ˇquick brown fox", Mode::HelixNormal);
+        cx.simulate_keystrokes("e");
+        cx.simulate_keystrokes("~");
+        cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal);
+    }
 }