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;