Add support for u and U in vim visual mode

Brooks Swinnerton created

Change summary

assets/keymaps/vim.json                              |  3 
crates/vim/src/normal.rs                             |  6 
crates/vim/src/normal/case.rs                        | 80 ++++++++++++-
crates/vim/test_data/test_convert_to_lower_case.json | 12 ++
crates/vim/test_data/test_convert_to_upper_case.json | 12 ++
5 files changed, 103 insertions(+), 10 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -400,7 +400,8 @@
   {
     "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
     "bindings": {
-      "u": "editor::Undo",
+      "u": "vim::ConvertToLowerCase",
+      "U": "vim::ConvertToUpperCase",
       "o": "vim::OtherEnd",
       "shift-o": "vim::OtherEnd",
       "d": "vim::VisualDelete",

crates/vim/src/normal.rs 🔗

@@ -26,7 +26,7 @@ use log::error;
 use workspace::Workspace;
 
 use self::{
-    case::change_case,
+    case::{change_case, convert_to_lower_case, convert_to_upper_case},
     change::{change_motion, change_object},
     delete::{delete_motion, delete_object},
     yank::{yank_motion, yank_object},
@@ -48,6 +48,8 @@ actions!(
         Yank,
         YankLine,
         ChangeCase,
+        ConvertToUpperCase,
+        ConvertToLowerCase,
         JoinLines,
     ]
 );
@@ -60,6 +62,8 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
     workspace.register_action(insert_line_above);
     workspace.register_action(insert_line_below);
     workspace.register_action(change_case);
+    workspace.register_action(convert_to_upper_case);
+    workspace.register_action(convert_to_lower_case);
     workspace.register_action(yank_line);
 
     workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {

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

@@ -3,9 +3,40 @@ use gpui::ViewContext;
 use language::{Bias, Point};
 use workspace::Workspace;
 
-use crate::{normal::ChangeCase, state::Mode, Vim};
+use crate::{
+    normal::ChangeCase, normal::ConvertToLowerCase, normal::ConvertToUpperCase, state::Mode, Vim,
+};
 
 pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
+    transform_case(cx, |c| {
+        if c.is_lowercase() {
+            c.to_uppercase().collect::<Vec<char>>()
+        } else {
+            c.to_lowercase().collect::<Vec<char>>()
+        }
+    })
+}
+
+pub fn convert_to_upper_case(
+    _: &mut Workspace,
+    _: &ConvertToUpperCase,
+    cx: &mut ViewContext<Workspace>,
+) {
+    transform_case(cx, |c| c.to_uppercase().collect::<Vec<char>>())
+}
+
+pub fn convert_to_lower_case(
+    _: &mut Workspace,
+    _: &ConvertToLowerCase,
+    cx: &mut ViewContext<Workspace>,
+) {
+    transform_case(cx, |c| c.to_lowercase().collect::<Vec<char>>())
+}
+
+fn transform_case<F>(cx: &mut ViewContext<Workspace>, transform: F)
+where
+    F: Fn(char) -> Vec<char> + Copy,
+{
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
         let count = vim.take_count(cx).unwrap_or(1) as u32;
@@ -54,13 +85,7 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
                         let text = snapshot
                             .text_for_range(range.start..range.end)
                             .flat_map(|s| s.chars())
-                            .flat_map(|c| {
-                                if c.is_lowercase() {
-                                    c.to_uppercase().collect::<Vec<char>>()
-                                } else {
-                                    c.to_lowercase().collect::<Vec<char>>()
-                                }
-                            })
+                            .flat_map(|c| transform(c))
                             .collect::<String>();
 
                         buffer.edit([(range, text)], None, cx)
@@ -74,6 +99,7 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
         vim.switch_mode(Mode::Normal, true, cx)
     })
 }
+
 #[cfg(test)]
 mod test {
     use crate::{state::Mode, test::NeovimBackedTestContext};
@@ -113,4 +139,42 @@ mod test {
         cx.simulate_keystroke("~");
         cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        // works in visual mode
+        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
+        cx.simulate_shared_keystrokes(["U"]).await;
+        cx.assert_shared_state("a😀CˇDÉ1*F\n").await;
+
+        // works with line selections
+        cx.set_shared_state("abˇC\n").await;
+        cx.simulate_shared_keystrokes(["shift-v", "U"]).await;
+        cx.assert_shared_state("ˇABC\n").await;
+
+        // works in visual block mode
+        cx.set_shared_state("ˇaa\nbb\ncc").await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "U"]).await;
+        cx.assert_shared_state("ˇAa\nBb\ncc").await;
+    }
+
+    #[gpui::test]
+    async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        // works in visual mode
+        cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
+        cx.simulate_shared_keystrokes(["u"]).await;
+        cx.assert_shared_state("A😀cˇdé1*f\n").await;
+
+        // works with line selections
+        cx.set_shared_state("ABˇc\n").await;
+        cx.simulate_shared_keystrokes(["shift-v", "u"]).await;
+        cx.assert_shared_state("ˇabc\n").await;
+
+        // works in visual block mode
+        cx.set_shared_state("ˇAa\nBb\nCc").await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "u"]).await;
+        cx.assert_shared_state("ˇaa\nbb\nCc").await;
+    }
 }

crates/vim/test_data/test_convert_to_lower_case.json 🔗

@@ -0,0 +1,12 @@
+{"Put":{"state":"A😀c«DÉ1*fˇ»\n"}}
+{"Key":"u"}
+{"Get":{"state":"A😀cˇdé1*f\n","mode":"Normal"}}
+{"Put":{"state":"ABˇc\n"}}
+{"Key":"shift-v"}
+{"Key":"u"}
+{"Get":{"state":"ˇabc\n","mode":"Normal"}}
+{"Put":{"state":"ˇAa\nBb\nCc"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"u"}
+{"Get":{"state":"ˇaa\nbb\nCc","mode":"Normal"}}

crates/vim/test_data/test_convert_to_upper_case.json 🔗

@@ -0,0 +1,12 @@
+{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
+{"Key":"U"}
+{"Get":{"state":"a😀CˇDÉ1*F\n","mode":"Normal"}}
+{"Put":{"state":"abˇC\n"}}
+{"Key":"shift-v"}
+{"Key":"U"}
+{"Get":{"state":"ˇABC\n","mode":"Normal"}}
+{"Put":{"state":"ˇaa\nbb\ncc"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"U"}
+{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}