diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 18b38384ef..8d933f19af 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -124,7 +124,6 @@ "g i": "vim::InsertAtPrevious", "g ,": "vim::ChangeListNewer", "g ;": "vim::ChangeListOlder", - "g q": "editor::Rewrap", "shift-h": "vim::WindowTop", "shift-m": "vim::WindowMiddle", "shift-l": "vim::WindowBottom", @@ -240,6 +239,8 @@ "g shift-u": ["vim::PushOperator", "Uppercase"], "g ~": ["vim::PushOperator", "OppositeCase"], "\"": ["vim::PushOperator", "Register"], + "g q": ["vim::PushOperator", "Rewrap"], + "g w": ["vim::PushOperator", "Rewrap"], "q": "vim::ToggleRecord", "shift-q": "vim::ReplayLastRecording", "@": ["vim::PushOperator", "ReplayRegister"], @@ -301,6 +302,7 @@ "i": ["vim::PushOperator", { "Object": { "around": false } }], "a": ["vim::PushOperator", { "Object": { "around": true } }], "g c": "vim::ToggleComments", + "g q": "vim::Rewrap", "\"": ["vim::PushOperator", "Register"], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", @@ -428,6 +430,15 @@ "~": "vim::CurrentLine" } }, + { + "context": "vim_operator == gq", + "bindings": { + "g q": "vim::CurrentLine", + "q": "vim::CurrentLine", + "g w": "vim::CurrentLine", + "w": "vim::CurrentLine" + } + }, { "context": "vim_operator == y", "bindings": { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 33eb51cb0e..1f4a9376d2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6705,6 +6705,10 @@ impl Editor { } pub fn rewrap(&mut self, _: &Rewrap, cx: &mut ViewContext) { + self.rewrap_impl(true, cx) + } + + pub fn rewrap_impl(&mut self, only_text: bool, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); let selections = self.selections.all::(cx); let mut selections = selections.iter().peekable(); @@ -6725,7 +6729,7 @@ impl Editor { continue; } - let mut should_rewrap = false; + let mut should_rewrap = !only_text; if let Some(language_scope) = buffer.language_scope_at(selection.head()) { match language_scope.language_name().0.as_ref() { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 741e09f178..10bf3c8e8d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -168,6 +168,7 @@ impl Vim { Some(Operator::Yank) => self.yank_motion(motion, times, cx), Some(Operator::AddSurrounds { target: None }) => {} Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx), + Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx), Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx), Some(Operator::Lowercase) => { self.change_case_motion(motion, times, CaseTarget::Lowercase, cx) @@ -199,6 +200,7 @@ impl Vim { Some(Operator::Outdent) => { self.indent_object(object, around, IndentDirection::Out, cx) } + Some(Operator::Rewrap) => self.rewrap_object(object, around, cx), Some(Operator::Lowercase) => { self.change_case_object(object, around, CaseTarget::Lowercase, cx) } @@ -478,8 +480,9 @@ impl Vim { } #[cfg(test)] mod test { - use gpui::{KeyBinding, TestAppContext}; + use gpui::{KeyBinding, TestAppContext, UpdateGlobal}; use indoc::indoc; + use language::language_settings::AllLanguageSettings; use settings::SettingsStore; use crate::{ @@ -1386,4 +1389,29 @@ mod test { cx.simulate_shared_keystrokes("2 0 r - ").await; cx.shared_state().await.assert_eq("ˇhello world\n"); } + + #[gpui::test] + async fn test_gq(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("textwidth=5").await; + + cx.update(|cx| { + SettingsStore::update_global(cx, |settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.defaults.preferred_line_length = Some(5); + }); + }) + }); + + cx.set_shared_state("ˇth th th th th th\n").await; + cx.simulate_shared_keystrokes("g q q").await; + cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n"); + + cx.set_shared_state("ˇth th th th th th\nth th th th th th\n") + .await; + cx.simulate_shared_keystrokes("v j g q").await; + cx.shared_state() + .await + .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n"); + } } diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs new file mode 100644 index 0000000000..3e61b3c3a1 --- /dev/null +++ b/crates/vim/src/rewrap.rs @@ -0,0 +1,114 @@ +use crate::{motion::Motion, object::Object, state::Mode, Vim}; +use collections::HashMap; +use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Bias, Editor}; +use gpui::actions; +use language::SelectionGoal; +use ui::ViewContext; + +actions!(vim, [Rewrap]); + +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &Rewrap, cx| { + vim.record_current_action(cx); + vim.take_count(cx); + vim.store_visual_marks(cx); + vim.update_editor(cx, |vim, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut positions = vim.save_selection_starts(editor, cx); + editor.rewrap_impl(false, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if let Some(anchor) = positions.remove(&selection.id) { + let mut point = anchor.to_display_point(map); + *point.column_mut() = 0; + selection.collapse_to(point, SelectionGoal::None); + } + }); + }); + }); + }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, true, cx) + } + }); +} + +impl Vim { + pub(crate) fn rewrap_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_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::Right); + selection_starts.insert(selection.id, anchor); + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); + }); + editor.rewrap_impl(false, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = selection_starts.remove(&selection.id).unwrap(); + let mut point = anchor.to_display_point(map); + *point.column_mut() = 0; + selection.collapse_to(point, SelectionGoal::None); + }); + }); + }); + }); + } + + pub(crate) fn rewrap_object( + &mut self, + object: Object, + around: bool, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_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| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); + original_positions.insert(selection.id, anchor); + object.expand_selection(map, selection, around); + }); + }); + editor.rewrap_impl(false, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = original_positions.remove(&selection.id).unwrap(); + let mut point = anchor.to_display_point(map); + *point.column_mut() = 0; + selection.collapse_to(point, SelectionGoal::None); + }); + }); + }); + }); + } +} + +#[cfg(test)] +mod test { + use crate::test::NeovimBackedTestContext; + + #[gpui::test] + async fn test_indent_gv(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("shiftwidth=4").await; + + cx.set_shared_state("ˇhello\nworld\n").await; + cx.simulate_shared_keystrokes("v j > g v").await; + cx.shared_state() + .await + .assert_eq("« hello\n ˇ» world\n"); + } +} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 1d642e990f..b61cb405e1 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -72,6 +72,7 @@ pub enum Operator { Jump { line: bool }, Indent, Outdent, + Rewrap, Lowercase, Uppercase, OppositeCase, @@ -454,6 +455,7 @@ impl Operator { Operator::Jump { line: true } => "'", Operator::Jump { line: false } => "`", Operator::Indent => ">", + Operator::Rewrap => "gq", Operator::Outdent => "<", Operator::Uppercase => "gU", Operator::Lowercase => "gu", @@ -482,6 +484,7 @@ impl Operator { Operator::Change | Operator::Delete | Operator::Yank + | Operator::Rewrap | Operator::Indent | Operator::Outdent | Operator::Lowercase diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a4b77b1a7a..701972c19b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -13,6 +13,7 @@ mod motion; mod normal; mod object; mod replace; +mod rewrap; mod state; mod surrounds; mod visual; @@ -291,6 +292,7 @@ impl Vim { command::register(editor, cx); replace::register(editor, cx); indent::register(editor, cx); + rewrap::register(editor, cx); object::register(editor, cx); visual::register(editor, cx); change_list::register(editor, cx); diff --git a/crates/vim/test_data/test_gq.json b/crates/vim/test_data/test_gq.json new file mode 100644 index 0000000000..08cdb12315 --- /dev/null +++ b/crates/vim/test_data/test_gq.json @@ -0,0 +1,12 @@ +{"SetOption":{"value":"textwidth=5"}} +{"Put":{"state":"ˇth th th th th th\n"}} +{"Key":"g"} +{"Key":"q"} +{"Key":"q"} +{"Get":{"state":"th th\nth th\nˇth th\n","mode":"Normal"}} +{"Put":{"state":"ˇth th th th th th\nth th th th th th\n"}} +{"Key":"v"} +{"Key":"j"} +{"Key":"g"} +{"Key":"q"} +{"Get":{"state":"th th\nth th\nth th\nth th\nth th\nˇth th\n","mode":"Normal"}}