From 57d7bc23aecd497f7e93a16b11b17b075daa425e Mon Sep 17 00:00:00 2001 From: 0x2CA <2478557459@qq.com> Date: Wed, 2 Apr 2025 10:17:00 +0800 Subject: [PATCH] vim: Add `g?` convert to `Rot13`/`Rot47` (#27824) Release Notes: - Added `g?` convert to `Rot13`/`Rot47` --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 11 ++ crates/editor/src/actions.rs | 2 + crates/editor/src/editor.rs | 36 +++++ crates/editor/src/element.rs | 2 + crates/gpui/src/keymap/context.rs | 2 +- crates/vim/src/normal.rs | 32 +++-- crates/vim/src/normal/{case.rs => convert.rs} | 128 ++++++++++++++++-- crates/vim/src/state.rs | 8 ++ crates/vim/src/vim.rs | 10 ++ .../test_data/test_change_rot13_motion.json | 23 ++++ .../test_data/test_change_rot13_object.json | 6 + .../vim/test_data/test_convert_to_rot13.json | 15 ++ 12 files changed, 252 insertions(+), 23 deletions(-) rename crates/vim/src/normal/{case.rs => convert.rs} (73%) create mode 100644 crates/vim/test_data/test_change_rot13_motion.json create mode 100644 crates/vim/test_data/test_change_rot13_object.json create mode 100644 crates/vim/test_data/test_convert_to_rot13.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index e219a5eee7..577374b2a5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -227,6 +227,8 @@ "g u": "vim::PushLowercase", "g shift-u": "vim::PushUppercase", "g ~": "vim::PushOppositeCase", + "g ?": "vim::PushRot13", + // "g ?": "vim::PushRot47", "\"": "vim::PushRegister", "g w": "vim::PushRewrap", "g q": "vim::PushRewrap", @@ -298,6 +300,8 @@ "g r": ["vim::Paste", { "preserve_clipboard": true }], "g c": "vim::ToggleComments", "g q": "vim::Rewrap", + "g ?": "vim::ConvertToRot13", + // "g ?": "vim::ConvertToRot47", "\"": "vim::PushRegister", // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", @@ -477,6 +481,13 @@ "~": "vim::CurrentLine" } }, + { + "context": "vim_operator == g?", + "bindings": { + "g ?": "vim::CurrentLine", + "?": "vim::CurrentLine" + } + }, { "context": "vim_operator == gq", "bindings": { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 5bd764eb1d..53a751a096 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -274,6 +274,8 @@ actions!( ConvertToTitleCase, ConvertToUpperCamelCase, ConvertToUpperCase, + ConvertToRot13, + ConvertToRot47, Copy, CopyAndTrim, CopyFileLocation, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 550b005250..cb516c59c1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9168,6 +9168,42 @@ impl Editor { }) } + pub fn convert_to_rot13( + &mut self, + _: &ConvertToRot13, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| match c { + 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, + 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, + _ => c, + }) + .collect() + }) + } + + pub fn convert_to_rot47( + &mut self, + _: &ConvertToRot47, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| { + let code_point = c as u32; + if code_point >= 33 && code_point <= 126 { + return char::from_u32(33 + ((code_point + 14) % 94)).unwrap(); + } + c + }) + .collect() + }) + } + fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) where Fn: FnMut(&str) -> String, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5745ee146a..10801912f7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -223,6 +223,8 @@ impl EditorElement { register_action(editor, window, Editor::convert_to_upper_camel_case); register_action(editor, window, Editor::convert_to_lower_camel_case); register_action(editor, window, Editor::convert_to_opposite_case); + register_action(editor, window, Editor::convert_to_rot13); + register_action(editor, window, Editor::convert_to_rot47); register_action(editor, window, Editor::delete_to_previous_word_start); register_action(editor, window, Editor::delete_to_previous_subword_start); register_action(editor, window, Editor::delete_to_next_word_end); diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 1732145d48..aff778e0a4 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -412,7 +412,7 @@ fn is_identifier_char(c: char) -> bool { } fn is_vim_operator_char(c: char) -> bool { - c == '>' || c == '<' || c == '~' || c == '"' + c == '>' || c == '<' || c == '~' || c == '"' || c == '?' } fn skip_whitespace(source: &str) -> &str { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 40fee8fe13..9fb719d04d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,5 +1,5 @@ -mod case; mod change; +mod convert; mod delete; mod increment; pub(crate) mod mark; @@ -22,8 +22,8 @@ use crate::{ state::{Mark, Mode, Operator}, surrounds::SurroundsType, }; -use case::CaseTarget; use collections::BTreeSet; +use convert::ConvertTarget; use editor::Anchor; use editor::Bias; use editor::Editor; @@ -55,6 +55,8 @@ actions!( ChangeCase, ConvertToUpperCase, ConvertToLowerCase, + ConvertToRot13, + ConvertToRot47, ToggleComments, ShowLocation, Undo, @@ -73,6 +75,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::change_case); Vim::action(editor, cx, Vim::convert_to_upper_case); Vim::action(editor, cx, Vim::convert_to_lower_case); + Vim::action(editor, cx, Vim::convert_to_rot13); + Vim::action(editor, cx, Vim::convert_to_rot47); Vim::action(editor, cx, Vim::yank_line); Vim::action(editor, cx, Vim::toggle_comments); Vim::action(editor, cx, Vim::paste); @@ -171,13 +175,19 @@ impl Vim { } Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, window, cx), Some(Operator::Lowercase) => { - self.change_case_motion(motion, times, CaseTarget::Lowercase, window, cx) + self.convert_motion(motion, times, ConvertTarget::LowerCase, window, cx) } Some(Operator::Uppercase) => { - self.change_case_motion(motion, times, CaseTarget::Uppercase, window, cx) + self.convert_motion(motion, times, ConvertTarget::UpperCase, window, cx) } Some(Operator::OppositeCase) => { - self.change_case_motion(motion, times, CaseTarget::OppositeCase, window, cx) + self.convert_motion(motion, times, ConvertTarget::OppositeCase, window, cx) + } + Some(Operator::Rot13) => { + self.convert_motion(motion, times, ConvertTarget::Rot13, window, cx) + } + Some(Operator::Rot47) => { + self.convert_motion(motion, times, ConvertTarget::Rot47, window, cx) } Some(Operator::ToggleComments) => { self.toggle_comments_motion(motion, times, window, cx) @@ -216,13 +226,19 @@ impl Vim { } Some(Operator::Rewrap) => self.rewrap_object(object, around, window, cx), Some(Operator::Lowercase) => { - self.change_case_object(object, around, CaseTarget::Lowercase, window, cx) + self.convert_object(object, around, ConvertTarget::LowerCase, window, cx) } Some(Operator::Uppercase) => { - self.change_case_object(object, around, CaseTarget::Uppercase, window, cx) + self.convert_object(object, around, ConvertTarget::UpperCase, window, cx) } Some(Operator::OppositeCase) => { - self.change_case_object(object, around, CaseTarget::OppositeCase, window, cx) + self.convert_object(object, around, ConvertTarget::OppositeCase, window, cx) + } + Some(Operator::Rot13) => { + self.convert_object(object, around, ConvertTarget::Rot13, window, cx) + } + Some(Operator::Rot47) => { + self.convert_object(object, around, ConvertTarget::Rot47, window, cx) } Some(Operator::AddSurrounds { target: None }) => { waiting_operator = Some(Operator::AddSurrounds { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/convert.rs similarity index 73% rename from crates/vim/src/normal/case.rs rename to crates/vim/src/normal/convert.rs index 8e6aa5f44d..af0154d3c2 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/convert.rs @@ -7,23 +7,25 @@ use multi_buffer::MultiBufferRow; use crate::{ Vim, motion::Motion, - normal::{ChangeCase, ConvertToLowerCase, ConvertToUpperCase}, + normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase}, object::Object, state::Mode, }; -pub enum CaseTarget { - Lowercase, - Uppercase, +pub enum ConvertTarget { + LowerCase, + UpperCase, OppositeCase, + Rot13, + Rot47, } impl Vim { - pub fn change_case_motion( + pub fn convert_motion( &mut self, motion: Motion, times: Option, - mode: CaseTarget, + mode: ConvertTarget, window: &mut Window, cx: &mut Context, ) { @@ -41,15 +43,21 @@ impl Vim { }); }); match mode { - CaseTarget::Lowercase => { + ConvertTarget::LowerCase => { editor.convert_to_lower_case(&Default::default(), window, cx) } - CaseTarget::Uppercase => { + ConvertTarget::UpperCase => { editor.convert_to_upper_case(&Default::default(), window, cx) } - CaseTarget::OppositeCase => { + ConvertTarget::OppositeCase => { editor.convert_to_opposite_case(&Default::default(), window, cx) } + ConvertTarget::Rot13 => { + editor.convert_to_rot13(&Default::default(), window, cx) + } + ConvertTarget::Rot47 => { + editor.convert_to_rot47(&Default::default(), window, cx) + } } editor.change_selections(None, window, cx, |s| { s.move_with(|map, selection| { @@ -62,11 +70,11 @@ impl Vim { }); } - pub fn change_case_object( + pub fn convert_object( &mut self, object: Object, around: bool, - mode: CaseTarget, + mode: ConvertTarget, window: &mut Window, cx: &mut Context, ) { @@ -85,15 +93,21 @@ impl Vim { }); }); match mode { - CaseTarget::Lowercase => { + ConvertTarget::LowerCase => { editor.convert_to_lower_case(&Default::default(), window, cx) } - CaseTarget::Uppercase => { + ConvertTarget::UpperCase => { editor.convert_to_upper_case(&Default::default(), window, cx) } - CaseTarget::OppositeCase => { + ConvertTarget::OppositeCase => { editor.convert_to_opposite_case(&Default::default(), window, cx) } + ConvertTarget::Rot13 => { + editor.convert_to_rot13(&Default::default(), window, cx) + } + ConvertTarget::Rot47 => { + editor.convert_to_rot47(&Default::default(), window, cx) + } } editor.change_selections(None, window, cx, |s| { s.move_with(|map, selection| { @@ -134,6 +148,36 @@ impl Vim { self.manipulate_text(window, cx, |c| c.to_lowercase().collect::>()) } + pub fn convert_to_rot13( + &mut self, + _: &ConvertToRot13, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |c| { + vec![match c { + 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, + 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, + _ => c, + }] + }) + } + + pub fn convert_to_rot47( + &mut self, + _: &ConvertToRot47, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |c| { + let code_point = c as u32; + if code_point >= 33 && code_point <= 126 { + return vec![char::from_u32(33 + ((code_point + 14) % 94)).unwrap()]; + } + vec![c] + }) + } + fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, transform: F) where F: Fn(char) -> Vec + Copy, @@ -308,4 +352,60 @@ mod test { cx.simulate_shared_keystrokes("g shift-u i w").await; cx.shared_state().await.assert_eq("abc ˇDEF\n"); } + + #[gpui::test] + async fn test_convert_to_rot13(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("g ?").await; + cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n"); + + // works with line selections + cx.set_shared_state("abˇC\n").await; + cx.simulate_shared_keystrokes("shift-v g ?").await; + cx.shared_state().await.assert_eq("ˇnoP\n"); + + // works in visual block mode + cx.set_shared_state("ˇaa\nbb\ncc").await; + cx.simulate_shared_keystrokes("ctrl-v j g ?").await; + cx.shared_state().await.assert_eq("ˇna\nob\ncc"); + } + + #[gpui::test] + async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇabc def").await; + cx.simulate_shared_keystrokes("g ? w").await; + cx.shared_state().await.assert_eq("ˇnop 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("ˇnop def"); + + cx.set_shared_state("abˇc def").await; + cx.simulate_shared_keystrokes("g ? i w").await; + cx.shared_state().await.assert_eq("ˇnop def"); + + cx.simulate_shared_keystrokes(".").await; + cx.shared_state().await.assert_eq("ˇabc def"); + + cx.simulate_shared_keystrokes("g ? $").await; + cx.shared_state().await.assert_eq("ˇnop qrs"); + } + + #[gpui::test] + async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .await; + cx.simulate_shared_keystrokes("g ? i w").await; + cx.shared_state() + .await + .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index d982d42211..6e7f753def 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -116,6 +116,8 @@ pub enum Operator { Lowercase, Uppercase, OppositeCase, + Rot13, + Rot47, Digraph { first_char: Option, }, @@ -958,6 +960,8 @@ impl Operator { Operator::Uppercase => "gU", Operator::Lowercase => "gu", Operator::OppositeCase => "g~", + Operator::Rot13 => "g?", + Operator::Rot47 => "g?", Operator::Register => "\"", Operator::RecordRegister => "q", Operator::ReplayRegister => "@", @@ -1006,6 +1010,8 @@ impl Operator { | Operator::ShellCommand | Operator::Lowercase | Operator::Uppercase + | Operator::Rot13 + | Operator::Rot47 | Operator::ReplaceWithRegister | Operator::Exchange | Operator::Object { .. } @@ -1026,6 +1032,8 @@ impl Operator { | Operator::Lowercase | Operator::Uppercase | Operator::OppositeCase + | Operator::Rot13 + | Operator::Rot47 | Operator::ToggleComments | Operator::ReplaceWithRegister | Operator::Rewrap diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 45e6f5cfb6..9a0991b653 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -153,6 +153,8 @@ actions!( PushLowercase, PushUppercase, PushOppositeCase, + PushRot13, + PushRot47, ToggleRegistersView, PushRegister, PushRecordRegister, @@ -619,6 +621,14 @@ impl Vim { vim.push_operator(Operator::OppositeCase, window, cx) }); + Vim::action(editor, cx, |vim, _: &PushRot13, window, cx| { + vim.push_operator(Operator::Rot13, window, cx) + }); + + Vim::action(editor, cx, |vim, _: &PushRot47, window, cx| { + vim.push_operator(Operator::Rot47, window, cx) + }); + Vim::action(editor, cx, |vim, _: &PushRegister, window, cx| { vim.push_operator(Operator::Register, window, cx) }); diff --git a/crates/vim/test_data/test_change_rot13_motion.json b/crates/vim/test_data/test_change_rot13_motion.json new file mode 100644 index 0000000000..62c39887ff --- /dev/null +++ b/crates/vim/test_data/test_change_rot13_motion.json @@ -0,0 +1,23 @@ +{"Put":{"state":"ˇabc def"}} +{"Key":"g"} +{"Key":"?"} +{"Key":"w"} +{"Get":{"state":"ˇnop def","mode":"Normal"}} +{"Key":"g"} +{"Key":"?"} +{"Key":"w"} +{"Get":{"state":"ˇabc def","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"ˇnop def","mode":"Normal"}} +{"Put":{"state":"abˇc def"}} +{"Key":"g"} +{"Key":"?"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"ˇnop def","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"ˇabc def","mode":"Normal"}} +{"Key":"g"} +{"Key":"?"} +{"Key":"$"} +{"Get":{"state":"ˇnop qrs","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_rot13_object.json b/crates/vim/test_data/test_change_rot13_object.json new file mode 100644 index 0000000000..19db51b946 --- /dev/null +++ b/crates/vim/test_data/test_change_rot13_object.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"}} +{"Key":"g"} +{"Key":"?"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_rot13.json b/crates/vim/test_data/test_convert_to_rot13.json new file mode 100644 index 0000000000..7ac67c885c --- /dev/null +++ b/crates/vim/test_data/test_convert_to_rot13.json @@ -0,0 +1,15 @@ +{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} +{"Key":"g"} +{"Key":"?"} +{"Get":{"state":"a😀CˇqÉ1*s\n","mode":"Normal"}} +{"Put":{"state":"abˇC\n"}} +{"Key":"shift-v"} +{"Key":"g"} +{"Key":"?"} +{"Get":{"state":"ˇnoP\n","mode":"Normal"}} +{"Put":{"state":"ˇaa\nbb\ncc"}} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"g"} +{"Key":"?"} +{"Get":{"state":"ˇna\nob\ncc","mode":"Normal"}}