surrounds.rs

   1use crate::{
   2    Vim,
   3    motion::{self, Motion},
   4    object::{Object, surrounding_markers},
   5    state::Mode,
   6};
   7use editor::{Bias, MultiBufferOffset, movement};
   8use gpui::{Context, Window};
   9use language::BracketPair;
  10
  11use std::sync::Arc;
  12
  13/// A char-based surround pair definition.
  14/// Single source of truth for all supported surround pairs.
  15#[derive(Clone, Copy, Debug, PartialEq)]
  16pub struct SurroundPair {
  17    pub open: char,
  18    pub close: char,
  19}
  20
  21impl SurroundPair {
  22    pub const fn new(open: char, close: char) -> Self {
  23        Self { open, close }
  24    }
  25
  26    pub fn to_bracket_pair(self) -> BracketPair {
  27        BracketPair {
  28            start: self.open.to_string(),
  29            end: self.close.to_string(),
  30            close: true,
  31            surround: true,
  32            newline: false,
  33        }
  34    }
  35
  36    pub fn to_object(self) -> Option<Object> {
  37        match self.open {
  38            '\'' => Some(Object::Quotes),
  39            '`' => Some(Object::BackQuotes),
  40            '"' => Some(Object::DoubleQuotes),
  41            '|' => Some(Object::VerticalBars),
  42            '(' => Some(Object::Parentheses),
  43            '[' => Some(Object::SquareBrackets),
  44            '{' => Some(Object::CurlyBrackets),
  45            '<' => Some(Object::AngleBrackets),
  46            _ => None,
  47        }
  48    }
  49}
  50
  51/// All supported surround pairs - single source of truth.
  52pub const SURROUND_PAIRS: &[SurroundPair] = &[
  53    SurroundPair::new('(', ')'),
  54    SurroundPair::new('[', ']'),
  55    SurroundPair::new('{', '}'),
  56    SurroundPair::new('<', '>'),
  57    SurroundPair::new('"', '"'),
  58    SurroundPair::new('\'', '\''),
  59    SurroundPair::new('`', '`'),
  60    SurroundPair::new('|', '|'),
  61];
  62
  63/// Bracket-only pairs for AnyBrackets matching.
  64const BRACKET_PAIRS: &[SurroundPair] = &[
  65    SurroundPair::new('(', ')'),
  66    SurroundPair::new('[', ']'),
  67    SurroundPair::new('{', '}'),
  68    SurroundPair::new('<', '>'),
  69];
  70
  71#[derive(Clone, Debug, PartialEq, Eq)]
  72pub enum SurroundsType {
  73    Motion(Motion),
  74    Object(Object, bool),
  75    Selection,
  76}
  77
  78impl Vim {
  79    pub fn add_surrounds(
  80        &mut self,
  81        text: Arc<str>,
  82        target: SurroundsType,
  83        window: &mut Window,
  84        cx: &mut Context<Self>,
  85    ) {
  86        self.stop_recording(cx);
  87        let count = Vim::take_count(cx);
  88        let forced_motion = Vim::take_forced_motion(cx);
  89        let mode = self.mode;
  90        self.update_editor(cx, |_, editor, cx| {
  91            let text_layout_details = editor.text_layout_details(window);
  92            editor.transact(window, cx, |editor, window, cx| {
  93                editor.set_clip_at_line_ends(false, cx);
  94
  95                let pair = bracket_pair_for_str_vim(&text);
  96                let surround = pair.end != surround_alias((*text).as_ref());
  97                let display_map = editor.display_snapshot(cx);
  98                let display_selections = editor.selections.all_adjusted_display(&display_map);
  99                let mut edits = Vec::new();
 100                let mut anchors = Vec::new();
 101
 102                for selection in &display_selections {
 103                    let range = match &target {
 104                        SurroundsType::Object(object, around) => {
 105                            object.range(&display_map, selection.clone(), *around, None)
 106                        }
 107                        SurroundsType::Motion(motion) => {
 108                            motion
 109                                .range(
 110                                    &display_map,
 111                                    selection.clone(),
 112                                    count,
 113                                    &text_layout_details,
 114                                    forced_motion,
 115                                )
 116                                .map(|(mut range, _)| {
 117                                    // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
 118                                    if let Motion::CurrentLine = motion {
 119                                        range.start = motion::first_non_whitespace(
 120                                            &display_map,
 121                                            false,
 122                                            range.start,
 123                                        );
 124                                        range.end = movement::saturating_right(
 125                                            &display_map,
 126                                            motion::last_non_whitespace(&display_map, range.end, 1),
 127                                        );
 128                                    }
 129                                    range
 130                                })
 131                        }
 132                        SurroundsType::Selection => Some(selection.range()),
 133                    };
 134
 135                    if let Some(range) = range {
 136                        let start = range.start.to_offset(&display_map, Bias::Right);
 137                        let end = range.end.to_offset(&display_map, Bias::Left);
 138                        let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
 139                            (format!("{}\n", pair.start), format!("\n{}", pair.end))
 140                        } else {
 141                            let maybe_space = if surround { " " } else { "" };
 142                            (
 143                                format!("{}{}", pair.start, maybe_space),
 144                                format!("{}{}", maybe_space, pair.end),
 145                            )
 146                        };
 147                        let start_anchor = display_map.buffer_snapshot().anchor_before(start);
 148
 149                        edits.push((start..start, start_cursor_str));
 150                        edits.push((end..end, end_cursor_str));
 151                        anchors.push(start_anchor..start_anchor);
 152                    } else {
 153                        let start_anchor = display_map
 154                            .buffer_snapshot()
 155                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
 156                        anchors.push(start_anchor..start_anchor);
 157                    }
 158                }
 159
 160                editor.edit(edits, cx);
 161                editor.set_clip_at_line_ends(true, cx);
 162                editor.change_selections(Default::default(), window, cx, |s| {
 163                    if mode == Mode::VisualBlock {
 164                        s.select_anchor_ranges(anchors.into_iter().take(1))
 165                    } else {
 166                        s.select_anchor_ranges(anchors)
 167                    }
 168                });
 169            });
 170        });
 171        self.switch_mode(Mode::Normal, false, window, cx);
 172    }
 173
 174    pub fn delete_surrounds(
 175        &mut self,
 176        text: Arc<str>,
 177        window: &mut Window,
 178        cx: &mut Context<Self>,
 179    ) {
 180        self.stop_recording(cx);
 181
 182        // only legitimate surrounds can be removed
 183        let Some(first_char) = text.chars().next() else {
 184            return;
 185        };
 186        let Some(surround_pair) = surround_pair_for_char_vim(first_char) else {
 187            return;
 188        };
 189        let Some(pair_object) = surround_pair.to_object() else {
 190            return;
 191        };
 192        let pair = surround_pair.to_bracket_pair();
 193        let surround = pair.end != *text;
 194
 195        self.update_editor(cx, |_, editor, cx| {
 196            editor.transact(window, cx, |editor, window, cx| {
 197                editor.set_clip_at_line_ends(false, cx);
 198
 199                let display_map = editor.display_snapshot(cx);
 200                let display_selections = editor.selections.all_display(&display_map);
 201                let mut edits = Vec::new();
 202                let mut anchors = Vec::new();
 203
 204                for selection in &display_selections {
 205                    let start = selection.start.to_offset(&display_map, Bias::Left);
 206                    if let Some(range) =
 207                        pair_object.range(&display_map, selection.clone(), true, None)
 208                    {
 209                        // If the current parenthesis object is single-line,
 210                        // then we need to filter whether it is the current line or not
 211                        if !pair_object.is_multiline() {
 212                            let is_same_row = selection.start.row() == range.start.row()
 213                                && selection.end.row() == range.end.row();
 214                            if !is_same_row {
 215                                anchors.push(start..start);
 216                                continue;
 217                            }
 218                        }
 219                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
 220                        // hello«ˇ  "hello in a word"  »again.
 221                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
 222                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
 223                        let mut chars_and_offset = display_map
 224                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 225                            .peekable();
 226                        while let Some((ch, offset)) = chars_and_offset.next() {
 227                            if ch.to_string() == pair.start {
 228                                let start = offset;
 229                                let mut end = start + 1usize;
 230                                if surround
 231                                    && let Some((next_ch, _)) = chars_and_offset.peek()
 232                                    && next_ch.eq(&' ')
 233                                {
 234                                    end += 1;
 235                                }
 236                                edits.push((start..end, ""));
 237                                anchors.push(start..start);
 238                                break;
 239                            }
 240                        }
 241                        let mut reverse_chars_and_offsets = display_map
 242                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
 243                            .peekable();
 244                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 245                            if ch.to_string() == pair.end {
 246                                let mut start = offset;
 247                                let end = start + 1usize;
 248                                if surround
 249                                    && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
 250                                    && next_ch.eq(&' ')
 251                                {
 252                                    start -= 1;
 253                                }
 254                                edits.push((start..end, ""));
 255                                break;
 256                            }
 257                        }
 258                    } else {
 259                        anchors.push(start..start);
 260                    }
 261                }
 262
 263                editor.change_selections(Default::default(), window, cx, |s| {
 264                    s.select_ranges(anchors);
 265                });
 266                edits.sort_by_key(|(range, _)| range.start);
 267                editor.edit(edits, cx);
 268                editor.set_clip_at_line_ends(true, cx);
 269            });
 270        });
 271    }
 272
 273    pub fn change_surrounds(
 274        &mut self,
 275        text: Arc<str>,
 276        target: Object,
 277        opening: bool,
 278        window: &mut Window,
 279        cx: &mut Context<Self>,
 280    ) {
 281        if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) {
 282            self.stop_recording(cx);
 283            self.update_editor(cx, |_, editor, cx| {
 284                editor.transact(window, cx, |editor, window, cx| {
 285                    editor.set_clip_at_line_ends(false, cx);
 286
 287                    let pair = bracket_pair_for_str_vim(&text);
 288
 289                    // A single space should be added if the new surround is a
 290                    // bracket and not a quote (pair.start != pair.end) and if
 291                    // the bracket used is the opening bracket.
 292                    let add_space =
 293                        !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref()));
 294
 295                    // Space should be preserved if either the surrounding
 296                    // characters being updated are quotes
 297                    // (will_replace_pair.start == will_replace_pair.end) or if
 298                    // the bracket used in the command is not an opening
 299                    // bracket.
 300                    let preserve_space =
 301                        will_replace_pair.start == will_replace_pair.end || !opening;
 302
 303                    let display_map = editor.display_snapshot(cx);
 304                    let selections = editor.selections.all_adjusted_display(&display_map);
 305                    let mut edits = Vec::new();
 306                    let mut anchors = Vec::new();
 307
 308                    for selection in &selections {
 309                        let start = selection.start.to_offset(&display_map, Bias::Left);
 310                        if let Some(range) =
 311                            target.range(&display_map, selection.clone(), true, None)
 312                        {
 313                            if !target.is_multiline() {
 314                                let is_same_row = selection.start.row() == range.start.row()
 315                                    && selection.end.row() == range.end.row();
 316                                if !is_same_row {
 317                                    anchors.push(start..start);
 318                                    continue;
 319                                }
 320                            }
 321
 322                            // Keeps track of the length of the string that is
 323                            // going to be edited on the start so we can ensure
 324                            // that the end replacement string does not exceed
 325                            // this value. Helpful when dealing with newlines.
 326                            let mut edit_len = 0;
 327                            let mut open_range_end = MultiBufferOffset(0);
 328                            let mut chars_and_offset = display_map
 329                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 330                                .peekable();
 331
 332                            while let Some((ch, offset)) = chars_and_offset.next() {
 333                                if ch.to_string() == will_replace_pair.start {
 334                                    let mut open_str = pair.start.clone();
 335                                    let start = offset;
 336                                    open_range_end = start + 1usize;
 337                                    while let Some((next_ch, _)) = chars_and_offset.next()
 338                                        && next_ch == ' '
 339                                    {
 340                                        open_range_end += 1;
 341
 342                                        if preserve_space {
 343                                            open_str.push(next_ch);
 344                                        }
 345                                    }
 346
 347                                    if add_space {
 348                                        open_str.push(' ');
 349                                    };
 350
 351                                    edit_len = open_range_end - start;
 352                                    edits.push((start..open_range_end, open_str));
 353                                    anchors.push(start..start);
 354                                    break;
 355                                }
 356                            }
 357
 358                            let mut reverse_chars_and_offsets = display_map
 359                                .reverse_buffer_chars_at(
 360                                    range.end.to_offset(&display_map, Bias::Left),
 361                                )
 362                                .peekable();
 363                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 364                                if ch.to_string() == will_replace_pair.end {
 365                                    let mut close_str = String::new();
 366                                    let mut start = offset;
 367                                    let end = start + 1usize;
 368                                    while let Some((next_ch, _)) = reverse_chars_and_offsets.next()
 369                                        && next_ch == ' '
 370                                        && close_str.len() < edit_len - 1
 371                                        && start > open_range_end
 372                                    {
 373                                        start -= 1;
 374
 375                                        if preserve_space {
 376                                            close_str.push(next_ch);
 377                                        }
 378                                    }
 379
 380                                    if add_space {
 381                                        close_str.push(' ');
 382                                    };
 383
 384                                    close_str.push_str(&pair.end);
 385                                    edits.push((start..end, close_str));
 386                                    break;
 387                                }
 388                            }
 389                        } else {
 390                            anchors.push(start..start);
 391                        }
 392                    }
 393
 394                    let stable_anchors = editor
 395                        .selections
 396                        .disjoint_anchors_arc()
 397                        .iter()
 398                        .map(|selection| {
 399                            let start = selection.start.bias_left(&display_map.buffer_snapshot());
 400                            start..start
 401                        })
 402                        .collect::<Vec<_>>();
 403                    edits.sort_by_key(|(range, _)| range.start);
 404                    editor.edit(edits, cx);
 405                    editor.set_clip_at_line_ends(true, cx);
 406                    editor.change_selections(Default::default(), window, cx, |s| {
 407                        s.select_anchor_ranges(stable_anchors);
 408                    });
 409                });
 410            });
 411        }
 412    }
 413
 414    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 415    ///
 416    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 417    /// A pair of brackets is considered valid if it is well-formed and properly closed.
 418    ///
 419    /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
 420    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
 421    pub fn check_and_move_to_valid_bracket_pair(
 422        &mut self,
 423        object: Object,
 424        window: &mut Window,
 425        cx: &mut Context<Self>,
 426    ) -> bool {
 427        let mut valid = false;
 428        if let Some(pair) = self.object_to_bracket_pair(object, cx) {
 429            self.update_editor(cx, |_, editor, cx| {
 430                editor.transact(window, cx, |editor, window, cx| {
 431                    editor.set_clip_at_line_ends(false, cx);
 432                    let display_map = editor.display_snapshot(cx);
 433                    let selections = editor.selections.all_adjusted_display(&display_map);
 434                    let mut anchors = Vec::new();
 435
 436                    for selection in &selections {
 437                        let start = selection.start.to_offset(&display_map, Bias::Left);
 438                        if let Some(range) =
 439                            object.range(&display_map, selection.clone(), true, None)
 440                        {
 441                            // If the current parenthesis object is single-line,
 442                            // then we need to filter whether it is the current line or not
 443                            if object.is_multiline()
 444                                || (!object.is_multiline()
 445                                    && selection.start.row() == range.start.row()
 446                                    && selection.end.row() == range.end.row())
 447                            {
 448                                valid = true;
 449                                let chars_and_offset = display_map
 450                                    .buffer_chars_at(
 451                                        range.start.to_offset(&display_map, Bias::Left),
 452                                    )
 453                                    .peekable();
 454                                for (ch, offset) in chars_and_offset {
 455                                    if ch.to_string() == pair.start {
 456                                        anchors.push(offset..offset);
 457                                        break;
 458                                    }
 459                                }
 460                            } else {
 461                                anchors.push(start..start)
 462                            }
 463                        } else {
 464                            anchors.push(start..start)
 465                        }
 466                    }
 467                    editor.change_selections(Default::default(), window, cx, |s| {
 468                        s.select_ranges(anchors);
 469                    });
 470                    editor.set_clip_at_line_ends(true, cx);
 471                });
 472            });
 473        }
 474        valid
 475    }
 476
 477    fn object_to_bracket_pair(
 478        &self,
 479        object: Object,
 480        cx: &mut Context<Self>,
 481    ) -> Option<BracketPair> {
 482        if let Some(pair) = object_to_surround_pair(object) {
 483            return Some(pair.to_bracket_pair());
 484        }
 485
 486        if object != Object::AnyBrackets {
 487            return None;
 488        }
 489
 490        // If we're dealing with `AnyBrackets`, which can map to multiple bracket
 491        // pairs, we'll need to first determine which `BracketPair` to target.
 492        // As such, we keep track of the smallest range size, so that in cases
 493        // like `({ name: "John" })` if the cursor is inside the curly brackets,
 494        // we target the curly brackets instead of the parentheses.
 495        let mut best_pair = None;
 496        let mut min_range_size = usize::MAX;
 497
 498        let _ = self.editor.update(cx, |editor, cx| {
 499            let display_map = editor.display_snapshot(cx);
 500            let selections = editor.selections.all_adjusted_display(&display_map);
 501            // Even if there's multiple cursors, we'll simply rely on the first one
 502            // to understand what bracket pair to map to. I believe we could, if
 503            // worth it, go one step above and have a `BracketPair` per selection, so
 504            // that `AnyBracket` could work in situations where the transformation
 505            // below could be done.
 506            //
 507            // ```
 508            // (< name:ˇ'Zed' >)
 509            // <[ name:ˇ'DeltaDB' ]>
 510            // ```
 511            //
 512            // After using `csb{`:
 513            //
 514            // ```
 515            // (ˇ{ name:'Zed' })
 516            // <ˇ{ name:'DeltaDB' }>
 517            // ```
 518            if let Some(selection) = selections.first() {
 519                let relative_to = selection.head();
 520                let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
 521
 522                for pair in BRACKET_PAIRS {
 523                    if let Some(range) = surrounding_markers(
 524                        &display_map,
 525                        relative_to,
 526                        true,
 527                        false,
 528                        pair.open,
 529                        pair.close,
 530                    ) {
 531                        let start_offset = range.start.to_offset(&display_map, Bias::Left);
 532                        let end_offset = range.end.to_offset(&display_map, Bias::Right);
 533
 534                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
 535                            let size = end_offset - start_offset;
 536                            if size < min_range_size {
 537                                min_range_size = size;
 538                                best_pair = Some(*pair);
 539                            }
 540                        }
 541                    }
 542                }
 543            }
 544        });
 545
 546        best_pair.map(|p| p.to_bracket_pair())
 547    }
 548}
 549
 550/// Convert an Object to its corresponding SurroundPair.
 551fn object_to_surround_pair(object: Object) -> Option<SurroundPair> {
 552    let open = match object {
 553        Object::Quotes => '\'',
 554        Object::BackQuotes => '`',
 555        Object::DoubleQuotes => '"',
 556        Object::VerticalBars => '|',
 557        Object::Parentheses => '(',
 558        Object::SquareBrackets => '[',
 559        Object::CurlyBrackets { .. } => '{',
 560        Object::AngleBrackets => '<',
 561        _ => return None,
 562    };
 563    surround_pair_for_char_vim(open)
 564}
 565
 566pub fn surround_alias(ch: &str) -> &str {
 567    match ch {
 568        "b" => ")",
 569        "B" => "}",
 570        "a" => ">",
 571        "r" => "]",
 572        _ => ch,
 573    }
 574}
 575
 576fn literal_surround_pair(ch: char) -> Option<SurroundPair> {
 577    SURROUND_PAIRS
 578        .iter()
 579        .find(|p| p.open == ch || p.close == ch)
 580        .copied()
 581}
 582
 583/// Resolve a character (including Vim aliases) to its surround pair.
 584/// Returns None for 'm' (match nearest) or unknown chars.
 585pub fn surround_pair_for_char_vim(ch: char) -> Option<SurroundPair> {
 586    let resolved = match ch {
 587        'b' => ')',
 588        'B' => '}',
 589        'r' => ']',
 590        'a' => '>',
 591        'm' => return None,
 592        _ => ch,
 593    };
 594    literal_surround_pair(resolved)
 595}
 596
 597/// Get a BracketPair for the given string, with fallback for unknown chars.
 598/// For vim surround operations that accept any character as a surround.
 599pub fn bracket_pair_for_str_vim(text: &str) -> BracketPair {
 600    text.chars()
 601        .next()
 602        .and_then(surround_pair_for_char_vim)
 603        .map(|p| p.to_bracket_pair())
 604        .unwrap_or_else(|| BracketPair {
 605            start: text.to_string(),
 606            end: text.to_string(),
 607            close: true,
 608            surround: true,
 609            newline: false,
 610        })
 611}
 612
 613/// Resolve a character to its surround pair using Helix semantics (no Vim aliases).
 614/// Returns None only for 'm' (match nearest). Unknown chars map to symmetric pairs.
 615pub fn surround_pair_for_char_helix(ch: char) -> Option<SurroundPair> {
 616    if ch == 'm' {
 617        return None;
 618    }
 619    literal_surround_pair(ch).or_else(|| Some(SurroundPair::new(ch, ch)))
 620}
 621
 622/// Get a BracketPair for the given string in Helix mode (literal, symmetric fallback).
 623pub fn bracket_pair_for_str_helix(text: &str) -> BracketPair {
 624    text.chars()
 625        .next()
 626        .and_then(surround_pair_for_char_helix)
 627        .map(|p| p.to_bracket_pair())
 628        .unwrap_or_else(|| BracketPair {
 629            start: text.to_string(),
 630            end: text.to_string(),
 631            close: true,
 632            surround: true,
 633            newline: false,
 634        })
 635}
 636
 637#[cfg(test)]
 638mod test {
 639    use gpui::KeyBinding;
 640    use indoc::indoc;
 641
 642    use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
 643
 644    #[gpui::test]
 645    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 646        let mut cx = VimTestContext::new(cx, true).await;
 647
 648        // test add surrounds with around
 649        cx.set_state(
 650            indoc! {"
 651            The quˇick brown
 652            fox jumps over
 653            the lazy dog."},
 654            Mode::Normal,
 655        );
 656        cx.simulate_keystrokes("y s i w {");
 657        cx.assert_state(
 658            indoc! {"
 659            The ˇ{ quick } brown
 660            fox jumps over
 661            the lazy dog."},
 662            Mode::Normal,
 663        );
 664
 665        // test add surrounds not with around
 666        cx.set_state(
 667            indoc! {"
 668            The quˇick brown
 669            fox jumps over
 670            the lazy dog."},
 671            Mode::Normal,
 672        );
 673        cx.simulate_keystrokes("y s i w }");
 674        cx.assert_state(
 675            indoc! {"
 676            The ˇ{quick} brown
 677            fox jumps over
 678            the lazy dog."},
 679            Mode::Normal,
 680        );
 681
 682        // test add surrounds with motion
 683        cx.set_state(
 684            indoc! {"
 685            The quˇick brown
 686            fox jumps over
 687            the lazy dog."},
 688            Mode::Normal,
 689        );
 690        cx.simulate_keystrokes("y s $ }");
 691        cx.assert_state(
 692            indoc! {"
 693            The quˇ{ick brown}
 694            fox jumps over
 695            the lazy dog."},
 696            Mode::Normal,
 697        );
 698
 699        // test add surrounds with multi cursor
 700        cx.set_state(
 701            indoc! {"
 702            The quˇick brown
 703            fox jumps over
 704            the laˇzy dog."},
 705            Mode::Normal,
 706        );
 707        cx.simulate_keystrokes("y s i w '");
 708        cx.assert_state(
 709            indoc! {"
 710            The ˇ'quick' brown
 711            fox jumps over
 712            the ˇ'lazy' dog."},
 713            Mode::Normal,
 714        );
 715
 716        // test multi cursor add surrounds with motion
 717        cx.set_state(
 718            indoc! {"
 719            The quˇick brown
 720            fox jumps over
 721            the laˇzy dog."},
 722            Mode::Normal,
 723        );
 724        cx.simulate_keystrokes("y s $ '");
 725        cx.assert_state(
 726            indoc! {"
 727            The quˇ'ick brown'
 728            fox jumps over
 729            the laˇ'zy dog.'"},
 730            Mode::Normal,
 731        );
 732
 733        // test multi cursor add surrounds with motion and custom string
 734        cx.set_state(
 735            indoc! {"
 736            The quˇick brown
 737            fox jumps over
 738            the laˇzy dog."},
 739            Mode::Normal,
 740        );
 741        cx.simulate_keystrokes("y s $ 1");
 742        cx.assert_state(
 743            indoc! {"
 744            The quˇ1ick brown1
 745            fox jumps over
 746            the laˇ1zy dog.1"},
 747            Mode::Normal,
 748        );
 749
 750        // test add surrounds with motion current line
 751        cx.set_state(
 752            indoc! {"
 753            The quˇick brown
 754            fox jumps over
 755            the lazy dog."},
 756            Mode::Normal,
 757        );
 758        cx.simulate_keystrokes("y s s {");
 759        cx.assert_state(
 760            indoc! {"
 761            ˇ{ The quick brown }
 762            fox jumps over
 763            the lazy dog."},
 764            Mode::Normal,
 765        );
 766
 767        cx.set_state(
 768            indoc! {"
 769                The quˇick brown•
 770            fox jumps over
 771            the lazy dog."},
 772            Mode::Normal,
 773        );
 774        cx.simulate_keystrokes("y s s {");
 775        cx.assert_state(
 776            indoc! {"
 777                ˇ{ The quick brown }•
 778            fox jumps over
 779            the lazy dog."},
 780            Mode::Normal,
 781        );
 782        cx.simulate_keystrokes("2 y s s )");
 783        cx.assert_state(
 784            indoc! {"
 785                ˇ({ The quick brown }•
 786            fox jumps over)
 787            the lazy dog."},
 788            Mode::Normal,
 789        );
 790
 791        // test add surrounds around object
 792        cx.set_state(
 793            indoc! {"
 794            The [quˇick] brown
 795            fox jumps over
 796            the lazy dog."},
 797            Mode::Normal,
 798        );
 799        cx.simulate_keystrokes("y s a ] )");
 800        cx.assert_state(
 801            indoc! {"
 802            The ˇ([quick]) brown
 803            fox jumps over
 804            the lazy dog."},
 805            Mode::Normal,
 806        );
 807
 808        // test add surrounds inside object
 809        cx.set_state(
 810            indoc! {"
 811            The [quˇick] brown
 812            fox jumps over
 813            the lazy dog."},
 814            Mode::Normal,
 815        );
 816        cx.simulate_keystrokes("y s i ] )");
 817        cx.assert_state(
 818            indoc! {"
 819            The [ˇ(quick)] brown
 820            fox jumps over
 821            the lazy dog."},
 822            Mode::Normal,
 823        );
 824    }
 825
 826    #[gpui::test]
 827    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 828        let mut cx = VimTestContext::new(cx, true).await;
 829
 830        cx.update(|_, cx| {
 831            cx.bind_keys([KeyBinding::new(
 832                "shift-s",
 833                PushAddSurrounds {},
 834                Some("vim_mode == visual"),
 835            )])
 836        });
 837
 838        // test add surrounds with around
 839        cx.set_state(
 840            indoc! {"
 841            The quˇick brown
 842            fox jumps over
 843            the lazy dog."},
 844            Mode::Normal,
 845        );
 846        cx.simulate_keystrokes("v i w shift-s {");
 847        cx.assert_state(
 848            indoc! {"
 849            The ˇ{ quick } brown
 850            fox jumps over
 851            the lazy dog."},
 852            Mode::Normal,
 853        );
 854
 855        // test add surrounds not with around
 856        cx.set_state(
 857            indoc! {"
 858            The quˇick brown
 859            fox jumps over
 860            the lazy dog."},
 861            Mode::Normal,
 862        );
 863        cx.simulate_keystrokes("v i w shift-s }");
 864        cx.assert_state(
 865            indoc! {"
 866            The ˇ{quick} brown
 867            fox jumps over
 868            the lazy dog."},
 869            Mode::Normal,
 870        );
 871
 872        // test add surrounds with motion
 873        cx.set_state(
 874            indoc! {"
 875            The quˇick brown
 876            fox jumps over
 877            the lazy dog."},
 878            Mode::Normal,
 879        );
 880        cx.simulate_keystrokes("v e shift-s }");
 881        cx.assert_state(
 882            indoc! {"
 883            The quˇ{ick} brown
 884            fox jumps over
 885            the lazy dog."},
 886            Mode::Normal,
 887        );
 888
 889        // test add surrounds with multi cursor
 890        cx.set_state(
 891            indoc! {"
 892            The quˇick brown
 893            fox jumps over
 894            the laˇzy dog."},
 895            Mode::Normal,
 896        );
 897        cx.simulate_keystrokes("v i w shift-s '");
 898        cx.assert_state(
 899            indoc! {"
 900            The ˇ'quick' brown
 901            fox jumps over
 902            the ˇ'lazy' dog."},
 903            Mode::Normal,
 904        );
 905
 906        // test add surrounds with visual block
 907        cx.set_state(
 908            indoc! {"
 909            The quˇick brown
 910            fox jumps over
 911            the lazy dog."},
 912            Mode::Normal,
 913        );
 914        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 915        cx.assert_state(
 916            indoc! {"
 917            The ˇ'quick' brown
 918            fox 'jumps' over
 919            the 'lazy 'dog."},
 920            Mode::Normal,
 921        );
 922
 923        // test add surrounds with visual line
 924        cx.set_state(
 925            indoc! {"
 926            The quˇick brown
 927            fox jumps over
 928            the lazy dog."},
 929            Mode::Normal,
 930        );
 931        cx.simulate_keystrokes("j shift-v shift-s '");
 932        cx.assert_state(
 933            indoc! {"
 934            The quick brown
 935            ˇ'
 936            fox jumps over
 937            '
 938            the lazy dog."},
 939            Mode::Normal,
 940        );
 941    }
 942
 943    #[gpui::test]
 944    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 945        let mut cx = VimTestContext::new(cx, true).await;
 946
 947        // test delete surround
 948        cx.set_state(
 949            indoc! {"
 950            The {quˇick} brown
 951            fox jumps over
 952            the lazy dog."},
 953            Mode::Normal,
 954        );
 955        cx.simulate_keystrokes("d s {");
 956        cx.assert_state(
 957            indoc! {"
 958            The ˇquick brown
 959            fox jumps over
 960            the lazy dog."},
 961            Mode::Normal,
 962        );
 963
 964        // test delete not exist surrounds
 965        cx.set_state(
 966            indoc! {"
 967            The {quˇick} brown
 968            fox jumps over
 969            the lazy dog."},
 970            Mode::Normal,
 971        );
 972        cx.simulate_keystrokes("d s [");
 973        cx.assert_state(
 974            indoc! {"
 975            The {quˇick} brown
 976            fox jumps over
 977            the lazy dog."},
 978            Mode::Normal,
 979        );
 980
 981        // test delete surround forward exist, in the surrounds plugin of other editors,
 982        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 983        cx.set_state(
 984            indoc! {"
 985            The {quick} brˇown
 986            fox jumps over
 987            the lazy dog."},
 988            Mode::Normal,
 989        );
 990        cx.simulate_keystrokes("d s {");
 991        cx.assert_state(
 992            indoc! {"
 993            The {quick} brˇown
 994            fox jumps over
 995            the lazy dog."},
 996            Mode::Normal,
 997        );
 998
 999        // test cursor delete inner surrounds
1000        cx.set_state(
1001            indoc! {"
1002            The { quick brown
1003            fox jumˇps over }
1004            the lazy dog."},
1005            Mode::Normal,
1006        );
1007        cx.simulate_keystrokes("d s {");
1008        cx.assert_state(
1009            indoc! {"
1010            The ˇquick brown
1011            fox jumps over
1012            the lazy dog."},
1013            Mode::Normal,
1014        );
1015
1016        // test multi cursor delete surrounds
1017        cx.set_state(
1018            indoc! {"
1019            The [quˇick] brown
1020            fox jumps over
1021            the [laˇzy] dog."},
1022            Mode::Normal,
1023        );
1024        cx.simulate_keystrokes("d s ]");
1025        cx.assert_state(
1026            indoc! {"
1027            The ˇquick brown
1028            fox jumps over
1029            the ˇlazy dog."},
1030            Mode::Normal,
1031        );
1032
1033        // test multi cursor delete surrounds with around
1034        cx.set_state(
1035            indoc! {"
1036            Tˇhe [ quick ] brown
1037            fox jumps over
1038            the [laˇzy] dog."},
1039            Mode::Normal,
1040        );
1041        cx.simulate_keystrokes("d s [");
1042        cx.assert_state(
1043            indoc! {"
1044            The ˇquick brown
1045            fox jumps over
1046            the ˇlazy dog."},
1047            Mode::Normal,
1048        );
1049
1050        cx.set_state(
1051            indoc! {"
1052            Tˇhe [ quick ] brown
1053            fox jumps over
1054            the [laˇzy ] dog."},
1055            Mode::Normal,
1056        );
1057        cx.simulate_keystrokes("d s [");
1058        cx.assert_state(
1059            indoc! {"
1060            The ˇquick brown
1061            fox jumps over
1062            the ˇlazy dog."},
1063            Mode::Normal,
1064        );
1065
1066        // test multi cursor delete different surrounds
1067        // the pair corresponding to the two cursors is the same,
1068        // so they are combined into one cursor
1069        cx.set_state(
1070            indoc! {"
1071            The [quˇick] brown
1072            fox jumps over
1073            the {laˇzy} dog."},
1074            Mode::Normal,
1075        );
1076        cx.simulate_keystrokes("d s {");
1077        cx.assert_state(
1078            indoc! {"
1079            The [quick] brown
1080            fox jumps over
1081            the ˇlazy dog."},
1082            Mode::Normal,
1083        );
1084
1085        // test delete surround with multi cursor and nest surrounds
1086        cx.set_state(
1087            indoc! {"
1088            fn test_surround() {
1089                ifˇ 2 > 1 {
1090                    ˇprintln!(\"it is fine\");
1091                };
1092            }"},
1093            Mode::Normal,
1094        );
1095        cx.simulate_keystrokes("d s }");
1096        cx.assert_state(
1097            indoc! {"
1098            fn test_surround() ˇ
1099                if 2 > 1 ˇ
1100                    println!(\"it is fine\");
1101                ;
1102            "},
1103            Mode::Normal,
1104        );
1105    }
1106
1107    #[gpui::test]
1108    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1109        let mut cx = VimTestContext::new(cx, true).await;
1110
1111        cx.set_state(
1112            indoc! {"
1113            The {quˇick} brown
1114            fox jumps over
1115            the lazy dog."},
1116            Mode::Normal,
1117        );
1118        cx.simulate_keystrokes("c s { [");
1119        cx.assert_state(
1120            indoc! {"
1121            The ˇ[ quick ] brown
1122            fox jumps over
1123            the lazy dog."},
1124            Mode::Normal,
1125        );
1126
1127        // test multi cursor change surrounds
1128        cx.set_state(
1129            indoc! {"
1130            The {quˇick} brown
1131            fox jumps over
1132            the {laˇzy} dog."},
1133            Mode::Normal,
1134        );
1135        cx.simulate_keystrokes("c s { [");
1136        cx.assert_state(
1137            indoc! {"
1138            The ˇ[ quick ] brown
1139            fox jumps over
1140            the ˇ[ lazy ] dog."},
1141            Mode::Normal,
1142        );
1143
1144        // test multi cursor delete different surrounds with after cursor
1145        cx.set_state(
1146            indoc! {"
1147            Thˇe {quick} brown
1148            fox jumps over
1149            the {laˇzy} dog."},
1150            Mode::Normal,
1151        );
1152        cx.simulate_keystrokes("c s { [");
1153        cx.assert_state(
1154            indoc! {"
1155            The ˇ[ quick ] brown
1156            fox jumps over
1157            the ˇ[ lazy ] dog."},
1158            Mode::Normal,
1159        );
1160
1161        // test multi cursor change surrount with not around
1162        cx.set_state(
1163            indoc! {"
1164            Thˇe { quick } brown
1165            fox jumps over
1166            the {laˇzy} dog."},
1167            Mode::Normal,
1168        );
1169        cx.simulate_keystrokes("c s { ]");
1170        cx.assert_state(
1171            indoc! {"
1172            The ˇ[quick] brown
1173            fox jumps over
1174            the ˇ[lazy] dog."},
1175            Mode::Normal,
1176        );
1177
1178        // test multi cursor change with not exist surround
1179        cx.set_state(
1180            indoc! {"
1181            The {quˇick} brown
1182            fox jumps over
1183            the [laˇzy] dog."},
1184            Mode::Normal,
1185        );
1186        cx.simulate_keystrokes("c s [ '");
1187        cx.assert_state(
1188            indoc! {"
1189            The {quick} brown
1190            fox jumps over
1191            the ˇ'lazy' dog."},
1192            Mode::Normal,
1193        );
1194
1195        // test change nesting surrounds
1196        cx.set_state(
1197            indoc! {"
1198            fn test_surround() {
1199                ifˇ 2 > 1 {
1200                    ˇprintln!(\"it is fine\");
1201                }
1202            };"},
1203            Mode::Normal,
1204        );
1205        cx.simulate_keystrokes("c s } ]");
1206        cx.assert_state(
1207            indoc! {"
1208            fn test_surround() ˇ[
1209                if 2 > 1 ˇ[
1210                    println!(\"it is fine\");
1211                ]
1212            ];"},
1213            Mode::Normal,
1214        );
1215
1216        // test spaces with quote change surrounds
1217        cx.set_state(
1218            indoc! {"
1219            fn test_surround() {
1220                \"ˇ \"
1221            };"},
1222            Mode::Normal,
1223        );
1224        cx.simulate_keystrokes("c s \" '");
1225        cx.assert_state(
1226            indoc! {"
1227            fn test_surround() {
1228                ˇ' '
1229            };"},
1230            Mode::Normal,
1231        );
1232
1233        // Currently, the same test case but using the closing bracket `]`
1234        // actually removes a whitespace before the closing bracket, something
1235        // that might need to be fixed?
1236        cx.set_state(
1237            indoc! {"
1238            fn test_surround() {
1239                ifˇ 2 > 1 {
1240                    ˇprintln!(\"it is fine\");
1241                }
1242            };"},
1243            Mode::Normal,
1244        );
1245        cx.simulate_keystrokes("c s { ]");
1246        cx.assert_state(
1247            indoc! {"
1248            fn test_surround() ˇ[
1249                if 2 > 1 ˇ[
1250                    println!(\"it is fine\");
1251                ]
1252            ];"},
1253            Mode::Normal,
1254        );
1255
1256        // test change quotes.
1257        cx.set_state(indoc! {"'  ˇstr  '"}, Mode::Normal);
1258        cx.simulate_keystrokes("c s ' \"");
1259        cx.assert_state(indoc! {"ˇ\"  str  \""}, Mode::Normal);
1260
1261        // test multi cursor change quotes
1262        cx.set_state(
1263            indoc! {"
1264            '  ˇstr  '
1265            some example text here
1266            ˇ'  str  '
1267        "},
1268            Mode::Normal,
1269        );
1270        cx.simulate_keystrokes("c s ' \"");
1271        cx.assert_state(
1272            indoc! {"
1273            ˇ\"  str  \"
1274            some example text here
1275            ˇ\"  str  \"
1276        "},
1277            Mode::Normal,
1278        );
1279
1280        // test quote to bracket spacing.
1281        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1282        cx.simulate_keystrokes("c s ' {");
1283        cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1284
1285        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1286        cx.simulate_keystrokes("c s ' }");
1287        cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1288    }
1289
1290    #[gpui::test]
1291    async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1292        let mut cx = VimTestContext::new(cx, true).await;
1293
1294        // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1295        // action.
1296        cx.update(|_, cx| {
1297            cx.bind_keys([KeyBinding::new(
1298                "b",
1299                AnyBrackets,
1300                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1301            )]);
1302        });
1303
1304        cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1305        cx.simulate_keystrokes("c s b [");
1306        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1307
1308        cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1309        cx.simulate_keystrokes("c s b {");
1310        cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1311
1312        cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1313        cx.simulate_keystrokes("c s b [");
1314        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1315
1316        cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1317        cx.simulate_keystrokes("c s b [");
1318        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1319
1320        cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1321        cx.simulate_keystrokes("c s b }");
1322        cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1323
1324        cx.set_state(
1325            indoc! {"
1326            (< name: ˇ'Zed' >)
1327            (< nˇame: 'DeltaDB' >)
1328        "},
1329            Mode::Normal,
1330        );
1331        cx.simulate_keystrokes("c s b {");
1332        cx.set_state(
1333            indoc! {"
1334            (ˇ{ name: 'Zed' })
1335            (ˇ{ name: 'DeltaDB' })
1336        "},
1337            Mode::Normal,
1338        );
1339    }
1340
1341    // The following test cases all follow tpope/vim-surround's behaviour
1342    // and are more focused on how whitespace is handled.
1343    #[gpui::test]
1344    async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
1345        let mut cx = VimTestContext::new(cx, true).await;
1346
1347        // Changing quote to quote should never change the surrounding
1348        // whitespace.
1349        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1350        cx.simulate_keystrokes("c s ' \"");
1351        cx.assert_state(indoc! {"ˇ\"  a  \""}, Mode::Normal);
1352
1353        cx.set_state(indoc! {"\"  ˇa  \""}, Mode::Normal);
1354        cx.simulate_keystrokes("c s \" '");
1355        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
1356
1357        // Changing quote to bracket adds one more space when the opening
1358        // bracket is used, does not affect whitespace when the closing bracket
1359        // is used.
1360        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1361        cx.simulate_keystrokes("c s ' {");
1362        cx.assert_state(indoc! {"ˇ{   a   }"}, Mode::Normal);
1363
1364        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1365        cx.simulate_keystrokes("c s ' }");
1366        cx.assert_state(indoc! {"ˇ{  a  }"}, Mode::Normal);
1367
1368        // Changing bracket to quote should remove all space when the
1369        // opening bracket is used and preserve all space when the
1370        // closing one is used.
1371        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1372        cx.simulate_keystrokes("c s { '");
1373        cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
1374
1375        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1376        cx.simulate_keystrokes("c s } '");
1377        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
1378
1379        // Changing bracket to bracket follows these rules:
1380        // * opening → opening – keeps only one space.
1381        // * opening → closing – removes all space.
1382        // * closing → opening – adds one space.
1383        // * closing → closing – does not change space.
1384        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
1385        cx.simulate_keystrokes("c s { [");
1386        cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1387
1388        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
1389        cx.simulate_keystrokes("c s { ]");
1390        cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
1391
1392        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1393        cx.simulate_keystrokes("c s } [");
1394        cx.assert_state(indoc! {"ˇ[   a   ]"}, Mode::Normal);
1395
1396        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1397        cx.simulate_keystrokes("c s } ]");
1398        cx.assert_state(indoc! {"ˇ[  a  ]"}, Mode::Normal);
1399    }
1400
1401    #[gpui::test]
1402    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1403        let mut cx = VimTestContext::new(cx, true).await;
1404
1405        cx.set_state(
1406            indoc! {"
1407            The quˇick brown
1408            fox jumps over
1409            the lazy dog."},
1410            Mode::Normal,
1411        );
1412        cx.simulate_keystrokes("y s i w [");
1413        cx.assert_state(
1414            indoc! {"
1415            The ˇ[ quick ] brown
1416            fox jumps over
1417            the lazy dog."},
1418            Mode::Normal,
1419        );
1420
1421        cx.simulate_keystrokes("c s [ }");
1422        cx.assert_state(
1423            indoc! {"
1424            The ˇ{quick} brown
1425            fox jumps over
1426            the lazy dog."},
1427            Mode::Normal,
1428        );
1429
1430        cx.simulate_keystrokes("d s {");
1431        cx.assert_state(
1432            indoc! {"
1433            The ˇquick brown
1434            fox jumps over
1435            the lazy dog."},
1436            Mode::Normal,
1437        );
1438
1439        cx.simulate_keystrokes("u");
1440        cx.assert_state(
1441            indoc! {"
1442            The ˇ{quick} brown
1443            fox jumps over
1444            the lazy dog."},
1445            Mode::Normal,
1446        );
1447    }
1448
1449    #[gpui::test]
1450    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1451        let mut cx = VimTestContext::new(cx, true).await;
1452
1453        // add aliases
1454        cx.set_state(
1455            indoc! {"
1456            The quˇick brown
1457            fox jumps over
1458            the lazy dog."},
1459            Mode::Normal,
1460        );
1461        cx.simulate_keystrokes("y s i w b");
1462        cx.assert_state(
1463            indoc! {"
1464            The ˇ(quick) brown
1465            fox jumps over
1466            the lazy dog."},
1467            Mode::Normal,
1468        );
1469
1470        cx.set_state(
1471            indoc! {"
1472            The quˇick brown
1473            fox jumps over
1474            the lazy dog."},
1475            Mode::Normal,
1476        );
1477        cx.simulate_keystrokes("y s i w B");
1478        cx.assert_state(
1479            indoc! {"
1480            The ˇ{quick} brown
1481            fox jumps over
1482            the lazy dog."},
1483            Mode::Normal,
1484        );
1485
1486        cx.set_state(
1487            indoc! {"
1488            The quˇick brown
1489            fox jumps over
1490            the lazy dog."},
1491            Mode::Normal,
1492        );
1493        cx.simulate_keystrokes("y s i w a");
1494        cx.assert_state(
1495            indoc! {"
1496            The ˇ<quick> brown
1497            fox jumps over
1498            the lazy dog."},
1499            Mode::Normal,
1500        );
1501
1502        cx.set_state(
1503            indoc! {"
1504            The quˇick brown
1505            fox jumps over
1506            the lazy dog."},
1507            Mode::Normal,
1508        );
1509        cx.simulate_keystrokes("y s i w r");
1510        cx.assert_state(
1511            indoc! {"
1512            The ˇ[quick] brown
1513            fox jumps over
1514            the lazy dog."},
1515            Mode::Normal,
1516        );
1517
1518        // change aliases
1519        cx.set_state(
1520            indoc! {"
1521            The {quˇick} brown
1522            fox jumps over
1523            the lazy dog."},
1524            Mode::Normal,
1525        );
1526        cx.simulate_keystrokes("c s { b");
1527        cx.assert_state(
1528            indoc! {"
1529            The ˇ(quick) brown
1530            fox jumps over
1531            the lazy dog."},
1532            Mode::Normal,
1533        );
1534
1535        cx.set_state(
1536            indoc! {"
1537            The (quˇick) brown
1538            fox jumps over
1539            the lazy dog."},
1540            Mode::Normal,
1541        );
1542        cx.simulate_keystrokes("c s ( B");
1543        cx.assert_state(
1544            indoc! {"
1545            The ˇ{quick} brown
1546            fox jumps over
1547            the lazy dog."},
1548            Mode::Normal,
1549        );
1550
1551        cx.set_state(
1552            indoc! {"
1553            The (quˇick) brown
1554            fox jumps over
1555            the lazy dog."},
1556            Mode::Normal,
1557        );
1558        cx.simulate_keystrokes("c s ( a");
1559        cx.assert_state(
1560            indoc! {"
1561            The ˇ<quick> brown
1562            fox jumps over
1563            the lazy dog."},
1564            Mode::Normal,
1565        );
1566
1567        cx.set_state(
1568            indoc! {"
1569            The <quˇick> brown
1570            fox jumps over
1571            the lazy dog."},
1572            Mode::Normal,
1573        );
1574        cx.simulate_keystrokes("c s < b");
1575        cx.assert_state(
1576            indoc! {"
1577            The ˇ(quick) brown
1578            fox jumps over
1579            the lazy dog."},
1580            Mode::Normal,
1581        );
1582
1583        cx.set_state(
1584            indoc! {"
1585            The (quˇick) brown
1586            fox jumps over
1587            the lazy dog."},
1588            Mode::Normal,
1589        );
1590        cx.simulate_keystrokes("c s ( r");
1591        cx.assert_state(
1592            indoc! {"
1593            The ˇ[quick] brown
1594            fox jumps over
1595            the lazy dog."},
1596            Mode::Normal,
1597        );
1598
1599        cx.set_state(
1600            indoc! {"
1601            The [quˇick] brown
1602            fox jumps over
1603            the lazy dog."},
1604            Mode::Normal,
1605        );
1606        cx.simulate_keystrokes("c s [ b");
1607        cx.assert_state(
1608            indoc! {"
1609            The ˇ(quick) brown
1610            fox jumps over
1611            the lazy dog."},
1612            Mode::Normal,
1613        );
1614
1615        // delete alias
1616        cx.set_state(
1617            indoc! {"
1618            The {quˇick} brown
1619            fox jumps over
1620            the lazy dog."},
1621            Mode::Normal,
1622        );
1623        cx.simulate_keystrokes("d s B");
1624        cx.assert_state(
1625            indoc! {"
1626            The ˇquick brown
1627            fox jumps over
1628            the lazy dog."},
1629            Mode::Normal,
1630        );
1631
1632        cx.set_state(
1633            indoc! {"
1634            The (quˇick) brown
1635            fox jumps over
1636            the lazy dog."},
1637            Mode::Normal,
1638        );
1639        cx.simulate_keystrokes("d s b");
1640        cx.assert_state(
1641            indoc! {"
1642            The ˇquick brown
1643            fox jumps over
1644            the lazy dog."},
1645            Mode::Normal,
1646        );
1647
1648        cx.set_state(
1649            indoc! {"
1650            The [quˇick] brown
1651            fox jumps over
1652            the lazy dog."},
1653            Mode::Normal,
1654        );
1655        cx.simulate_keystrokes("d s r");
1656        cx.assert_state(
1657            indoc! {"
1658            The ˇquick brown
1659            fox jumps over
1660            the lazy dog."},
1661            Mode::Normal,
1662        );
1663
1664        cx.set_state(
1665            indoc! {"
1666            The <quˇick> brown
1667            fox jumps over
1668            the lazy dog."},
1669            Mode::Normal,
1670        );
1671        cx.simulate_keystrokes("d s a");
1672        cx.assert_state(
1673            indoc! {"
1674            The ˇquick brown
1675            fox jumps over
1676            the lazy dog."},
1677            Mode::Normal,
1678        );
1679    }
1680
1681    #[test]
1682    fn test_surround_pair_for_char() {
1683        use super::{SURROUND_PAIRS, surround_pair_for_char_helix, surround_pair_for_char_vim};
1684
1685        fn as_tuple(pair: Option<super::SurroundPair>) -> Option<(char, char)> {
1686            pair.map(|p| (p.open, p.close))
1687        }
1688
1689        assert_eq!(as_tuple(surround_pair_for_char_vim('b')), Some(('(', ')')));
1690        assert_eq!(as_tuple(surround_pair_for_char_vim('B')), Some(('{', '}')));
1691        assert_eq!(as_tuple(surround_pair_for_char_vim('r')), Some(('[', ']')));
1692        assert_eq!(as_tuple(surround_pair_for_char_vim('a')), Some(('<', '>')));
1693
1694        assert_eq!(surround_pair_for_char_vim('m'), None);
1695
1696        for pair in SURROUND_PAIRS {
1697            assert_eq!(
1698                as_tuple(surround_pair_for_char_vim(pair.open)),
1699                Some((pair.open, pair.close))
1700            );
1701            assert_eq!(
1702                as_tuple(surround_pair_for_char_vim(pair.close)),
1703                Some((pair.open, pair.close))
1704            );
1705        }
1706
1707        // Test unknown char returns None
1708        assert_eq!(surround_pair_for_char_vim('x'), None);
1709
1710        // Helix resolves literal chars and falls back to symmetric pairs.
1711        assert_eq!(
1712            as_tuple(surround_pair_for_char_helix('*')),
1713            Some(('*', '*'))
1714        );
1715        assert_eq!(surround_pair_for_char_helix('m'), None);
1716    }
1717}