surrounds.rs

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