vim: Add gU/gu/g~ (#12782)

Co-Authored-By: ethanmsl@gmail.com

Release Notes:

- vim: Added `gu`/`gU`/`g~` for changing case. (#12565)
This commit is contained in:
Conrad Irwin 2024-06-07 12:38:12 -06:00 committed by GitHub
parent 3eac83eece
commit 5548773b2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 168 additions and 3 deletions

View File

@ -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

View File

@ -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)),

View File

@ -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");
}
}

View File

@ -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~",
}
}

View File

@ -539,6 +539,9 @@ impl Vim {
| Operator::Replace
| Operator::Indent
| Operator::Outdent
| Operator::Lowercase
| Operator::Uppercase
| Operator::OppositeCase
) {
self.start_recording(cx)
};

View File

@ -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"}}