Detailed changes
@@ -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
@@ -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)),
@@ -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<usize>,
+ 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<Workspace>) {
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");
+ }
}
@@ -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~",
}
}
@@ -539,6 +539,9 @@ impl Vim {
| Operator::Replace
| Operator::Indent
| Operator::Outdent
+ | Operator::Lowercase
+ | Operator::Uppercase
+ | Operator::OppositeCase
) {
self.start_recording(cx)
};
@@ -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"}}