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