From 77b2da2b4254f855e50933ef180c813d0f8d01e4 Mon Sep 17 00:00:00 2001 From: Benjamin Davies Date: Tue, 25 Jun 2024 03:29:06 +1200 Subject: [PATCH] vim: Surround in visual mode (#13347) Adds support for surrounding text in visual/visual-line/visual-block mode by re-using the `AddSurrounds` operator. There is no default binding though so the user must follow the instructions to enable it. Note that the behaviour varies slightly for the visual-line and visual-block modes. In visual-line mode the surrounds are placed on separate lines (the vim-surround extension also indents the contents but I opted not to as that behaviour is less important with the use of code formatters). In visual-block mode each of the selected regions is surrounded and the cursor returns to the beginning of the selection after the action is complete. Release Notes: - Added action to surround text in visual mode (no default binding). Fixes #13122 --- crates/vim/src/state.rs | 5 +- crates/vim/src/surrounds.rs | 149 ++++++++++++++++++++++++++++++++++-- crates/vim/src/vim.rs | 6 +- docs/src/vim.md | 16 ++++ 4 files changed, 167 insertions(+), 9 deletions(-) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index d94695c4aa..a22f3bd820 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -287,7 +287,10 @@ impl EditorState { .unwrap_or_else(|| "none"), ); - if self.mode == Mode::Replace { + if self.mode == Mode::Replace + || (matches!(active_operator, Some(Operator::AddSurrounds { .. })) + && self.mode.is_visual()) + { context.add("VimWaiting"); } context diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 14093d896f..9245b2be97 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -9,10 +9,12 @@ use gpui::WindowContext; use language::BracketPair; use serde::Deserialize; use std::sync::Arc; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum SurroundsType { Motion(Motion), Object(Object), + Selection, } // This exists so that we can have Deserialize on Operators, but not on Motions. @@ -29,6 +31,7 @@ pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowConte Vim::update(cx, |vim, cx| { vim.stop_recording(); let count = vim.take_count(cx); + let mode = vim.state().mode; vim.update_active_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { @@ -84,19 +87,25 @@ pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowConte }); range } + SurroundsType::Selection => Some(selection.range()), }; if let Some(range) = range { let start = range.start.to_offset(&display_map, Bias::Right); let end = range.end.to_offset(&display_map, Bias::Left); - let start_cursor_str = - format!("{}{}", pair.start, if surround { " " } else { "" }); - let close_cursor_str = - format!("{}{}", if surround { " " } else { "" }, pair.end); + let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine { + (format!("{}\n", pair.start), format!("{}\n", pair.end)) + } else { + let maybe_space = if surround { " " } else { "" }; + ( + format!("{}{}", pair.start, maybe_space), + format!("{}{}", maybe_space, pair.end), + ) + }; let start_anchor = display_map.buffer_snapshot.anchor_before(start); edits.push((start..start, start_cursor_str)); - edits.push((end..end, close_cursor_str)); + edits.push((end..end, end_cursor_str)); anchors.push(start_anchor..start_anchor); } else { let start_anchor = display_map @@ -111,7 +120,11 @@ pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowConte }); editor.set_clip_at_line_ends(true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_anchor_ranges(anchors) + if mode == Mode::VisualBlock { + s.select_anchor_ranges(anchors.into_iter().take(1)) + } else { + s.select_anchor_ranges(anchors) + } }); }); }); @@ -530,9 +543,14 @@ fn object_to_bracket_pair(object: Object) -> Option { #[cfg(test)] mod test { + use gpui::KeyBinding; use indoc::indoc; - use crate::{state::Mode, test::VimTestContext}; + use crate::{ + state::{Mode, Operator}, + test::VimTestContext, + PushOperator, + }; #[gpui::test] async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { @@ -682,6 +700,123 @@ mod test { ); } + #[gpui::test] + async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "shift-s", + PushOperator(Operator::AddSurrounds { target: None }), + Some("vim_mode == visual"), + )]) + }); + + // test add surrounds with arround + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("v i w shift-s {"); + cx.assert_state( + indoc! {" + The ˇ{ quick } brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + + // test add surrounds not with arround + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("v i w shift-s }"); + cx.assert_state( + indoc! {" + The ˇ{quick} brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + + // test add surrounds with motion + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("v e shift-s }"); + cx.assert_state( + indoc! {" + The quˇ{ick} brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + + // test add surrounds with multi cursor + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the laˇzy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("v i w shift-s '"); + cx.assert_state( + indoc! {" + The ˇ'quick' brown + fox jumps over + the ˇ'lazy' dog."}, + Mode::Normal, + ); + + // test add surrounds with visual block + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("ctrl-v i w j j shift-s '"); + cx.assert_state( + indoc! {" + The ˇ'quick' brown + fox 'jumps' over + the 'lazy 'dog."}, + Mode::Normal, + ); + + // test add surrounds with visual line + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::Normal, + ); + cx.simulate_keystrokes("j shift-v shift-s '"); + cx.assert_state( + indoc! {" + The quick brown + ˇ' + fox jumps over + ' + the lazy dog."}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a027059ea3..6134e4d7e9 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -39,7 +39,7 @@ use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; use state::{EditorState, Mode, Operator, RecordedSelection, Register, WorkspaceState}; use std::{ops::Range, sync::Arc}; -use surrounds::{add_surrounds, change_surrounds, delete_surrounds}; +use surrounds::{add_surrounds, change_surrounds, delete_surrounds, SurroundsType}; use ui::BorrowAppContext; use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; @@ -861,6 +861,10 @@ impl Vim { Vim::update(cx, |vim, cx| vim.clear_operator(cx)); } } + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + add_surrounds(text, SurroundsType::Selection, cx); + Vim::update(cx, |vim, cx| vim.clear_operator(cx)); + } _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, Some(Operator::ChangeSurrounds { target }) => match Vim::read(cx).state().mode { diff --git a/docs/src/vim.md b/docs/src/vim.md index 68174b0cad..6f907e904e 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -293,6 +293,22 @@ Subword motion is not enabled by default. To enable it, add these bindings to yo }, ``` +Surrounding the selection in visual mode is also not enabled by default (`shift-s` normally behaves like `c`). To enable it, add the following to your keymap. + +```json + { + "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", + "bindings": { + "shift-s": [ + "vim::PushOperator", + { + "AddSurrounds": {} + } + ] + } + } +``` + ## Supported plugins Zed has nascent support for some Vim plugins: