surrounds.rs

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