From e5a968b709feeeac1e12de054645671d64071676 Mon Sep 17 00:00:00 2001 From: Dino Date: Wed, 3 Sep 2025 00:03:14 +0100 Subject: [PATCH] vim: Fix change surround with any brackets text object (#37386) This commit fixes an issue with how the `AnyBrackets` object was handled with change surrounds (`cs`). With the keymap below, if one was to use `csb{` with the text `(bracketed)` and the cursor inside the parentheses, the text would not change. ```json { "context": "vim_operator == a || vim_operator == i || vim_operator == cs", "bindings": { "b": "vim::AnyBrackets" } } ``` Unfortunately there was no implementation for finding a corresponding `BracketPair` for the `AnyBrackets` object, meaning that, when using `cs` (change surrounds) the code would simply do nothing. This commit updates this logic so as to try and find the nearest surrounding bracket (parentheses, curly brackets, square brackets or angle brackets), ensuring that `cs` also works with `AnyBrackets`. Closes #24439 Release Notes: - Fixed handling of `AnyBrackets` in vim's change surrounds (`cs`) --- crates/vim/src/object.rs | 2 +- crates/vim/src/surrounds.rs | 255 ++++++++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 67 deletions(-) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 693de9f6971ff02639eee33e29e22e14902a7d37..366acb740bca32f5e191dd22309dd026c0d7ddd3 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1510,7 +1510,7 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D map.max_point() } -fn surrounding_markers( +pub fn surrounding_markers( map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool, diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 83500cf88b56c8f556887eb874901f50b6178018..7c36ebe6747488376d2264e4984175fb536fed4f 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -1,7 +1,7 @@ use crate::{ Vim, motion::{self, Motion}, - object::Object, + object::{Object, surrounding_markers}, state::Mode, }; use editor::{Bias, movement}; @@ -224,7 +224,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - if let Some(will_replace_pair) = object_to_bracket_pair(target) { + if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) { self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -358,7 +358,7 @@ impl Vim { cx: &mut Context, ) -> bool { let mut valid = false; - if let Some(pair) = object_to_bracket_pair(object) { + if let Some(pair) = self.object_to_bracket_pair(object, cx) { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -405,6 +405,140 @@ impl Vim { } valid } + + fn object_to_bracket_pair( + &self, + object: Object, + cx: &mut Context, + ) -> Option { + match object { + Object::Quotes => Some(BracketPair { + start: "'".to_string(), + end: "'".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::BackQuotes => Some(BracketPair { + start: "`".to_string(), + end: "`".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::DoubleQuotes => Some(BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::VerticalBars => Some(BracketPair { + start: "|".to_string(), + end: "|".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::Parentheses => Some(BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::SquareBrackets => Some(BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::CurlyBrackets => Some(BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::AngleBrackets => Some(BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::AnyBrackets => { + // If we're dealing with `AnyBrackets`, which can map to multiple + // bracket pairs, we'll need to first determine which `BracketPair` to + // target. + // As such, we keep track of the smallest range size, so + // that in cases like `({ name: "John" })` if the cursor is + // inside the curly brackets, we target the curly brackets + // instead of the parentheses. + let mut bracket_pair = None; + let mut min_range_size = usize::MAX; + + let _ = self.editor.update(cx, |editor, cx| { + let (display_map, selections) = editor.selections.all_adjusted_display(cx); + // Even if there's multiple cursors, we'll simply rely on + // the first one to understand what bracket pair to map to. + // I believe we could, if worth it, go one step above and + // have a `BracketPair` per selection, so that `AnyBracket` + // could work in situations where the transformation below + // could be done. + // + // ``` + // (< name:ˇ'Zed' >) + // <[ name:ˇ'DeltaDB' ]> + // ``` + // + // After using `csb{`: + // + // ``` + // (ˇ{ name:'Zed' }) + // <ˇ{ name:'DeltaDB' }> + // ``` + if let Some(selection) = selections.first() { + let relative_to = selection.head(); + let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; + let cursor_offset = relative_to.to_offset(&display_map, Bias::Left); + + for &(open, close) in bracket_pairs.iter() { + if let Some(range) = surrounding_markers( + &display_map, + relative_to, + true, + false, + open, + close, + ) { + let start_offset = range.start.to_offset(&display_map, Bias::Left); + let end_offset = range.end.to_offset(&display_map, Bias::Right); + + if cursor_offset >= start_offset && cursor_offset <= end_offset { + let size = end_offset - start_offset; + if size < min_range_size { + min_range_size = size; + bracket_pair = Some(BracketPair { + start: open.to_string(), + end: close.to_string(), + close: true, + surround: true, + newline: false, + }) + } + } + } + } + } + }); + + bracket_pair + } + _ => None, + } + } } fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { @@ -505,74 +639,12 @@ fn pair_to_object(pair: &BracketPair) -> Option { } } -fn object_to_bracket_pair(object: Object) -> Option { - match object { - Object::Quotes => Some(BracketPair { - start: "'".to_string(), - end: "'".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::BackQuotes => Some(BracketPair { - start: "`".to_string(), - end: "`".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::DoubleQuotes => Some(BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::VerticalBars => Some(BracketPair { - start: "|".to_string(), - end: "|".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::Parentheses => Some(BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::SquareBrackets => Some(BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::CurlyBrackets => Some(BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AngleBrackets => Some(BracketPair { - start: "<".to_string(), - end: ">".to_string(), - close: true, - surround: true, - newline: false, - }), - _ => None, - } -} - #[cfg(test)] mod test { use gpui::KeyBinding; use indoc::indoc; - use crate::{PushAddSurrounds, state::Mode, test::VimTestContext}; + use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext}; #[gpui::test] async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { @@ -1171,6 +1243,57 @@ mod test { ); } + #[gpui::test] + async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Update keybindings so that using `csb` triggers Vim's `AnyBrackets` + // action. + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "b", + AnyBrackets, + Some("vim_operator == a || vim_operator == i || vim_operator == cs"), + )]); + }); + + cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal); + cx.simulate_keystrokes("c s b {"); + cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal); + + cx.set_state(indoc! {""}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); + cx.simulate_keystrokes("c s b {"); + cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); + + cx.set_state( + indoc! {" + (< name: ˇ'Zed' >) + (< nˇame: 'DeltaDB' >) + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("c s b {"); + cx.set_state( + indoc! {" + (ˇ{ name: 'Zed' }) + (ˇ{ name: 'DeltaDB' }) + "}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await;