@@ -4,6 +4,7 @@ use crate::{
Vim,
motion::{is_subword_end, is_subword_start, right},
state::{Mode, Operator},
+ surrounds::{BRACKET_PAIRS, QUOTE_PAIRS, SurroundPair},
};
use editor::{
Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
@@ -606,7 +607,6 @@ impl Object {
surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
}
Object::AnyQuotes => {
- let quote_types = ['\'', '"', '`'];
let cursor_offset = relative_to.to_offset(map, Bias::Left);
// Find innermost range directly without collecting all ranges
@@ -614,14 +614,14 @@ impl Object {
let mut min_size = usize::MAX;
// First pass: find innermost enclosing range
- for quote in quote_types {
+ for &SurroundPair { open, close } in QUOTE_PAIRS {
if let Some(range) = surrounding_markers(
map,
relative_to,
around,
self.is_multiline(),
- quote,
- quote,
+ open,
+ close,
) {
let start_offset = range.start.to_offset(map, Bias::Left);
let end_offset = range.end.to_offset(map, Bias::Right);
@@ -641,16 +641,16 @@ impl Object {
}
// Fallback: find nearest pair if not inside any quotes
- quote_types
+ QUOTE_PAIRS
.iter()
- .flat_map(|"e| {
+ .flat_map(|&SurroundPair { open, close }| {
surrounding_markers(
map,
relative_to,
around,
self.is_multiline(),
- quote,
- quote,
+ open,
+ close,
)
})
.min_by_key(|range| {
@@ -681,14 +681,13 @@ impl Object {
surrounding_html_tag(map, head, range, around)
}
Object::AnyBrackets => {
- let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
let cursor_offset = relative_to.to_offset(map, Bias::Left);
// Find innermost enclosing bracket range
let mut innermost = None;
let mut min_size = usize::MAX;
- for &(open, close) in bracket_pairs.iter() {
+ for &SurroundPair { open, close } in BRACKET_PAIRS {
if let Some(range) = surrounding_markers(
map,
relative_to,
@@ -715,9 +714,9 @@ impl Object {
}
// Fallback: find nearest bracket pair if not inside any
- bracket_pairs
+ BRACKET_PAIRS
.iter()
- .flat_map(|&(open, close)| {
+ .flat_map(|&SurroundPair { open, close }| {
surrounding_markers(
map,
relative_to,
@@ -61,13 +61,20 @@ pub const SURROUND_PAIRS: &[SurroundPair] = &[
];
/// Bracket-only pairs for AnyBrackets matching.
-const BRACKET_PAIRS: &[SurroundPair] = &[
+pub const BRACKET_PAIRS: &[SurroundPair] = &[
SurroundPair::new('(', ')'),
SurroundPair::new('[', ']'),
SurroundPair::new('{', '}'),
SurroundPair::new('<', '>'),
];
+/// Quote-only pairs for AnyQuotes matching.
+pub const QUOTE_PAIRS: &[SurroundPair] = &[
+ SurroundPair::new('"', '"'),
+ SurroundPair::new('\'', '\''),
+ SurroundPair::new('`', '`'),
+];
+
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType {
Motion(Motion),
@@ -472,10 +479,19 @@ impl Vim {
return Some(pair.to_bracket_pair());
}
- if object != Object::AnyBrackets {
- return None;
+ match object {
+ Object::AnyBrackets => self.any_pair(BRACKET_PAIRS, cx),
+ Object::AnyQuotes => self.any_pair(QUOTE_PAIRS, cx),
+ Object::MiniQuotes | Object::MiniBrackets => self.mini_pair(object, cx),
+ _ => None,
}
+ }
+ fn any_pair(
+ &self,
+ allowed_pairs: &[SurroundPair],
+ cx: &mut Context<Self>,
+ ) -> Option<BracketPair> {
// 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
@@ -508,7 +524,7 @@ impl Vim {
let relative_to = selection.head();
let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
- for pair in BRACKET_PAIRS {
+ for pair in allowed_pairs {
if let Some(range) = surrounding_markers(
&display_map,
relative_to,
@@ -534,6 +550,25 @@ impl Vim {
best_pair.map(|p| p.to_bracket_pair())
}
+
+ fn mini_pair(&self, object: Object, cx: &mut Context<Self>) -> Option<BracketPair> {
+ self.editor
+ .update(cx, |editor, cx| {
+ let display_map = editor.display_snapshot(cx);
+ let selections = editor.selections.all_adjusted_display(&display_map);
+ // For now, only primary selection is used to select the bracket/quote pair. It might be weird
+ // if multi-select resulted in different quote kinds being replaced for different selections.
+ // any_pair uses the same logic, so this should be consistent across {Any,Mini}{Quotes,Brackets}
+ let selection = selections.first()?.clone();
+ let range = object.range(&display_map, selection, true, None)?;
+ let start_offset = range.start.to_offset(&display_map, Bias::Left);
+ let (pair_char, _) = display_map.buffer_chars_at(start_offset).next()?;
+ literal_surround_pair(pair_char)
+ })
+ .ok()
+ .flatten()
+ .map(|surround| surround.to_bracket_pair())
+ }
}
/// Convert an Object to its corresponding SurroundPair.
@@ -628,7 +663,12 @@ mod test {
use gpui::KeyBinding;
use indoc::indoc;
- use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
+ use crate::{
+ PushAddSurrounds,
+ object::{AnyBrackets, AnyQuotes, MiniBrackets, MiniQuotes},
+ state::Mode,
+ test::VimTestContext,
+ };
#[gpui::test]
async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
@@ -1335,6 +1375,108 @@ mod test {
);
}
+ #[gpui::test]
+ async fn test_change_surrounds_mini_brackets(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ // Update keybindings so that using `csb` triggers Vim's `MiniBrackets` action.
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "b",
+ MiniBrackets,
+ 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! {"<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! {"(<ˇZed>)"}, Mode::Normal);
+ cx.simulate_keystrokes("c s b )");
+ cx.assert_state(indoc! {"(ˇ(Zed))"}, Mode::Normal);
+
+ cx.set_state(
+ indoc! {"
+ (<ˇZed>)
+ (<ˇDeltaDB>)
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("c s b (");
+ cx.assert_state(
+ indoc! {"
+ (ˇ( Zed ))
+ (ˇ( DeltaDB ))
+ "},
+ Mode::Normal,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_surrounds_any_quotes(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ // Update keybindings so that using `csq` triggers Vim's `AnyQuotes` action.
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "q",
+ AnyQuotes,
+ Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
+ )]);
+ });
+
+ cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
+ cx.simulate_keystrokes("c s q \"");
+ cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
+
+ cx.set_state(indoc! {"` ˇstr `"}, Mode::Normal);
+ cx.simulate_keystrokes("c s q '");
+ cx.assert_state(indoc! {"ˇ' str '"}, Mode::Normal);
+
+ cx.set_state(indoc! {"\" ˇstr \""}, Mode::Normal);
+ cx.simulate_keystrokes("c s q `");
+ cx.assert_state(indoc! {"ˇ` str `"}, Mode::Normal);
+ }
+
+ #[gpui::test]
+ async fn test_change_surrounds_mini_quotes(cx: &mut gpui::TestAppContext) {
+ // NOTE: needs TypeScript test cx to recognize single/backquotes
+ let mut cx = VimTestContext::new_typescript(cx).await;
+
+ // Update keybindings so that using `csq` triggers Vim's `MiniQuotes` action.
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "q",
+ MiniQuotes,
+ Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
+ )]);
+ });
+ cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
+ cx.simulate_keystrokes("c s q \"");
+ cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
+
+ cx.set_state(indoc! {"` ˇstr `"}, Mode::Normal);
+ cx.simulate_keystrokes("c s q '");
+ cx.assert_state(indoc! {"ˇ' str '"}, Mode::Normal);
+
+ cx.set_state(indoc! {"\" ˇstr \""}, Mode::Normal);
+ cx.simulate_keystrokes("c s q `");
+ cx.assert_state(indoc! {"ˇ` str `"}, Mode::Normal);
+ }
+
// The following test cases all follow tpope/vim-surround's behaviour
// and are more focused on how whitespace is handled.
#[gpui::test]