diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9f8b087cb8..9b6d0819d6 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -381,6 +381,9 @@ "shift-s": "vim::SubstituteLine", ">": ["vim::PushOperator", "Indent"], "<": ["vim::PushOperator", "Outdent"], + "g u": ["vim::PushOperator", "Lowercase"], + "g shift-u": ["vim::PushOperator", "Uppercase"], + "g ~": ["vim::PushOperator", "OppositeCase"], "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-pageup": "pane::ActivatePrevItem", // tree-sitter related commands diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2f02fbb624..1bd7040ed5 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -21,6 +21,7 @@ use crate::{ surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType}, Vim, }; +use case::{change_case_motion, change_case_object, CaseTarget}; use collections::BTreeSet; use editor::display_map::ToDisplayPoint; use editor::scroll::Autoscroll; @@ -198,6 +199,15 @@ pub fn normal_motion( Some(Operator::AddSurrounds { target: None }) => {} Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx), Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx), + Some(Operator::Lowercase) => { + change_case_motion(vim, motion, times, CaseTarget::Lowercase, cx) + } + Some(Operator::Uppercase) => { + change_case_motion(vim, motion, times, CaseTarget::Uppercase, cx) + } + Some(Operator::OppositeCase) => { + change_case_motion(vim, motion, times, CaseTarget::OppositeCase, cx) + } Some(operator) => { // Can't do anything for text objects, Ignoring error!("Unexpected normal mode motion operator: {:?}", operator) @@ -220,6 +230,15 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) { Some(Operator::Outdent) => { indent_object(vim, object, around, IndentDirection::Out, cx) } + Some(Operator::Lowercase) => { + change_case_object(vim, object, around, CaseTarget::Lowercase, cx) + } + Some(Operator::Uppercase) => { + change_case_object(vim, object, around, CaseTarget::Uppercase, cx) + } + Some(Operator::OppositeCase) => { + change_case_object(vim, object, around, CaseTarget::OppositeCase, cx) + } Some(Operator::AddSurrounds { target: None }) => { waiting_operator = Some(Operator::AddSurrounds { target: Some(SurroundsType::Object(object)), diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 6b0336c216..71c206dcc8 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -1,13 +1,98 @@ -use editor::scroll::Autoscroll; +use collections::HashMap; +use editor::{display_map::ToDisplayPoint, scroll::Autoscroll}; use gpui::ViewContext; -use language::{Bias, Point}; +use language::{Bias, Point, SelectionGoal}; use multi_buffer::MultiBufferRow; +use ui::WindowContext; use workspace::Workspace; use crate::{ - normal::ChangeCase, normal::ConvertToLowerCase, normal::ConvertToUpperCase, state::Mode, Vim, + motion::Motion, + normal::{ChangeCase, ConvertToLowerCase, ConvertToUpperCase}, + object::Object, + state::Mode, + Vim, }; +pub enum CaseTarget { + Lowercase, + Uppercase, + OppositeCase, +} + +pub fn change_case_motion( + vim: &mut Vim, + motion: Motion, + times: Option, + mode: CaseTarget, + cx: &mut WindowContext, +) { + vim.stop_recording(); + vim.update_active_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + let mut selection_starts: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); + selection_starts.insert(selection.id, anchor); + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); + }); + match mode { + CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), + CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), + CaseTarget::OppositeCase => { + editor.convert_to_opposite_case(&Default::default(), cx) + } + } + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = selection_starts.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); + }); + }); + }); +} + +pub fn change_case_object( + vim: &mut Vim, + object: Object, + around: bool, + mode: CaseTarget, + cx: &mut WindowContext, +) { + vim.stop_recording(); + vim.update_active_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + object.expand_selection(map, selection, around); + original_positions.insert( + selection.id, + map.display_point_to_anchor(selection.start, Bias::Left), + ); + }); + }); + match mode { + CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), + CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), + CaseTarget::OppositeCase => { + editor.convert_to_opposite_case(&Default::default(), cx) + } + } + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); + }); + }); + }); +} + pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { manipulate_text(cx, |c| { if c.is_lowercase() { @@ -180,4 +265,29 @@ mod test { cx.simulate_shared_keystrokes("ctrl-v j u").await; cx.shared_state().await.assert_eq("ˇaa\nbb\nCc"); } + + #[gpui::test] + async fn test_change_case_motion(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + // works in visual mode + cx.set_shared_state("ˇabc def").await; + cx.simulate_shared_keystrokes("g shift-u w").await; + cx.shared_state().await.assert_eq("ˇABC def"); + + cx.simulate_shared_keystrokes("g u w").await; + cx.shared_state().await.assert_eq("ˇabc def"); + + cx.simulate_shared_keystrokes("g ~ w").await; + cx.shared_state().await.assert_eq("ˇABC def"); + + cx.simulate_shared_keystrokes(".").await; + cx.shared_state().await.assert_eq("ˇabc def"); + + cx.set_shared_state("abˇc def").await; + cx.simulate_shared_keystrokes("g ~ i w").await; + cx.shared_state().await.assert_eq("ˇABC def"); + + cx.simulate_shared_keystrokes(".").await; + cx.shared_state().await.assert_eq("ˇabc def"); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 4d606a56e7..122cc068e0 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -63,6 +63,10 @@ pub enum Operator { Jump { line: bool }, Indent, Outdent, + + Lowercase, + Uppercase, + OppositeCase, } #[derive(Default, Clone)] @@ -270,6 +274,9 @@ impl Operator { Operator::Jump { line: false } => "`", Operator::Indent => ">", Operator::Outdent => "<", + Operator::Uppercase => "gU", + Operator::Lowercase => "gu", + Operator::OppositeCase => "g~", } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c38e65fad8..f0980d925a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -539,6 +539,9 @@ impl Vim { | Operator::Replace | Operator::Indent | Operator::Outdent + | Operator::Lowercase + | Operator::Uppercase + | Operator::OppositeCase ) { self.start_recording(cx) }; diff --git a/crates/vim/test_data/test_change_case_motion.json b/crates/vim/test_data/test_change_case_motion.json new file mode 100644 index 0000000000..4d3600508f --- /dev/null +++ b/crates/vim/test_data/test_change_case_motion.json @@ -0,0 +1,23 @@ +{"Put":{"state":"ˇabc def"}} +{"Key":"g"} +{"Key":"shift-u"} +{"Key":"w"} +{"Get":{"state":"ˇABC def","mode":"Normal"}} +{"Key":"g"} +{"Key":"u"} +{"Key":"w"} +{"Get":{"state":"ˇabc def","mode":"Normal"}} +{"Key":"g"} +{"Key":"~"} +{"Key":"w"} +{"Get":{"state":"ˇABC def","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"ˇabc def","mode":"Normal"}} +{"Put":{"state":"abˇc def"}} +{"Key":"g"} +{"Key":"~"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"ˇABC def","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"ˇabc def","mode":"Normal"}}