Adjust the yss surrounds operator (#11212)

Hans and Conrad Irwin created

For #11084 In the case of an indentation in front of the current line,
it may also be necessary to deal with the start point of the selected
range


Release Notes:


- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/vim/src/motion.rs                            | 33 ++++++++
crates/vim/src/surrounds.rs                         | 58 +++++++++++---
crates/vim/src/vim.rs                               |  2 
crates/vim/test_data/test_end_of_line_downward.json |  8 ++
4 files changed, 87 insertions(+), 14 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -447,6 +447,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
         vim.clear_operator(cx);
         if let Some(operator) = waiting_operator {
             vim.push_operator(operator, cx);
+            vim.update_state(|state| state.pre_count = count)
         }
     });
 }
@@ -755,7 +756,7 @@ impl Motion {
             },
             NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
             StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
-            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
+            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
             GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
             WindowTop => window_top(map, point, &text_layout_details, times - 1),
             WindowMiddle => window_middle(map, point, &text_layout_details),
@@ -1422,6 +1423,26 @@ pub(crate) fn first_non_whitespace(
     start_offset.to_display_point(map)
 }
 
+pub(crate) fn last_non_whitespace(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    count: usize,
+) -> DisplayPoint {
+    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
+    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
+    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
+        if ch == '\n' {
+            break;
+        }
+        end_of_line = offset;
+        if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
+            break;
+        }
+    }
+
+    end_of_line.to_display_point(map)
+}
+
 pub(crate) fn start_of_line(
     map: &DisplaySnapshot,
     display_lines: bool,
@@ -1899,6 +1920,16 @@ mod test {
         cx.assert_shared_state("one\n  ˇtwo\nthree").await;
     }
 
+    #[gpui::test]
+    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("ˇ one \n two \nthree").await;
+        cx.simulate_shared_keystrokes(["g", "_"]).await;
+        cx.assert_shared_state(" onˇe \n two \nthree").await;
+        cx.simulate_shared_keystrokes(["2", "g", "_"]).await;
+        cx.assert_shared_state(" one \n twˇo \nthree").await;
+    }
+
     #[gpui::test]
     async fn test_window_top(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/surrounds.rs 🔗

@@ -1,4 +1,9 @@
-use crate::{motion::Motion, object::Object, state::Mode, Vim};
+use crate::{
+    motion::{self, Motion},
+    object::Object,
+    state::Mode,
+    Vim,
+};
 use editor::{movement, scroll::Autoscroll, Bias};
 use gpui::WindowContext;
 use language::BracketPair;
@@ -23,6 +28,7 @@ impl<'de> Deserialize<'de> for SurroundsType {
 pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.stop_recording();
+        let count = vim.take_count(cx);
         vim.update_active_editor(cx, |_, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
@@ -52,22 +58,26 @@ pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowConte
                                 .range(
                                     &display_map,
                                     selection.clone(),
-                                    Some(1),
+                                    count,
                                     true,
                                     &text_layout_details,
                                 )
                                 .map(|mut range| {
-                                    // The Motion::CurrentLine operation will contain the newline of the current line,
-                                    // so we need to deal with this edge case
+                                    // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
                                     if let Motion::CurrentLine = motion {
-                                        let offset = range.end.to_offset(&display_map, Bias::Left);
-                                        if let Some((last_ch, _)) =
-                                            display_map.reverse_buffer_chars_at(offset).next()
-                                        {
-                                            if last_ch == '\n' {
-                                                range.end = movement::left(&display_map, range.end);
-                                            }
-                                        }
+                                        range.start = motion::first_non_whitespace(
+                                            &display_map,
+                                            false,
+                                            range.start,
+                                        );
+                                        range.end = movement::saturating_right(
+                                            &display_map,
+                                            motion::last_non_whitespace(
+                                                &display_map,
+                                                movement::left(&display_map, range.end),
+                                                1,
+                                            ),
+                                        );
                                     }
                                     range
                                 });
@@ -627,6 +637,30 @@ mod test {
             the lazy dog."},
             Mode::Normal,
         );
+
+        cx.set_state(
+            indoc! {"
+                The quˇick brown•
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["y", "s", "s", "{"]);
+        cx.assert_state(
+            indoc! {"
+                ˇ{ The quick brown }•
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["2", "y", "s", "s", ")"]);
+        cx.assert_state(
+            indoc! {"
+                ˇ({ The quick brown }•
+            fox jumps over)
+            the lazy dog."},
+            Mode::Normal,
+        );
     }
 
     #[gpui::test]

crates/vim/src/vim.rs 🔗

@@ -526,7 +526,7 @@ impl Vim {
                 | Operator::ChangeSurrounds { .. }
                 | Operator::DeleteSurrounds
         ) {
-            self.clear_operator(cx);
+            self.update_state(|state| state.operator_stack.clear());
         };
         self.update_state(|state| state.operator_stack.push(operator));
         self.sync_vim_settings(cx);

crates/vim/test_data/test_end_of_line_downward.json 🔗

@@ -0,0 +1,8 @@
+{"Put":{"state":"ˇ one \n two \nthree"}}
+{"Key":"g"}
+{"Key":"_"}
+{"Get":{"state":" onˇe \n two \nthree","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"g"}
+{"Key":"_"}
+{"Get":{"state":" one \n twˇo \nthree","mode":"Normal"}}