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                                true,
 546                                true,
 547                                false,
 548                                open,
 549                                close,
 550                            ) {
 551                                let start_offset = range.start.to_offset(&display_map, Bias::Left);
 552                                let end_offset = range.end.to_offset(&display_map, Bias::Right);
 553
 554                                if cursor_offset >= start_offset && cursor_offset <= end_offset {
 555                                    let size = end_offset - start_offset;
 556                                    if size < min_range_size {
 557                                        min_range_size = size;
 558                                        bracket_pair = Some(BracketPair {
 559                                            start: open.to_string(),
 560                                            end: close.to_string(),
 561                                            close: true,
 562                                            surround: true,
 563                                            newline: false,
 564                                        })
 565                                    }
 566                                }
 567                            }
 568                        }
 569                    }
 570                });
 571
 572                bracket_pair
 573            }
 574            _ => None,
 575        }
 576    }
 577}
 578
 579fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 580    pairs
 581        .iter()
 582        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 583}
 584
 585fn surround_alias(ch: &str) -> &str {
 586    match ch {
 587        "b" => ")",
 588        "B" => "}",
 589        "a" => ">",
 590        "r" => "]",
 591        _ => ch,
 592    }
 593}
 594
 595fn all_support_surround_pair() -> Vec<BracketPair> {
 596    vec![
 597        BracketPair {
 598            start: "{".into(),
 599            end: "}".into(),
 600            close: true,
 601            surround: true,
 602            newline: false,
 603        },
 604        BracketPair {
 605            start: "'".into(),
 606            end: "'".into(),
 607            close: true,
 608            surround: true,
 609            newline: false,
 610        },
 611        BracketPair {
 612            start: "`".into(),
 613            end: "`".into(),
 614            close: true,
 615            surround: true,
 616            newline: false,
 617        },
 618        BracketPair {
 619            start: "\"".into(),
 620            end: "\"".into(),
 621            close: true,
 622            surround: true,
 623            newline: false,
 624        },
 625        BracketPair {
 626            start: "(".into(),
 627            end: ")".into(),
 628            close: true,
 629            surround: true,
 630            newline: false,
 631        },
 632        BracketPair {
 633            start: "|".into(),
 634            end: "|".into(),
 635            close: true,
 636            surround: true,
 637            newline: false,
 638        },
 639        BracketPair {
 640            start: "[".into(),
 641            end: "]".into(),
 642            close: true,
 643            surround: true,
 644            newline: false,
 645        },
 646        BracketPair {
 647            start: "<".into(),
 648            end: ">".into(),
 649            close: true,
 650            surround: true,
 651            newline: false,
 652        },
 653    ]
 654}
 655
 656fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 657    match pair.start.as_str() {
 658        "'" => Some(Object::Quotes),
 659        "`" => Some(Object::BackQuotes),
 660        "\"" => Some(Object::DoubleQuotes),
 661        "|" => Some(Object::VerticalBars),
 662        "(" => Some(Object::Parentheses),
 663        "[" => Some(Object::SquareBrackets),
 664        "{" => Some(Object::CurlyBrackets),
 665        "<" => Some(Object::AngleBrackets),
 666        _ => None,
 667    }
 668}
 669
 670#[cfg(test)]
 671mod test {
 672    use gpui::KeyBinding;
 673    use indoc::indoc;
 674
 675    use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};
 676
 677    #[gpui::test]
 678    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 679        let mut cx = VimTestContext::new(cx, true).await;
 680
 681        // test add surrounds with around
 682        cx.set_state(
 683            indoc! {"
 684            The quˇick brown
 685            fox jumps over
 686            the lazy dog."},
 687            Mode::Normal,
 688        );
 689        cx.simulate_keystrokes("y s i w {");
 690        cx.assert_state(
 691            indoc! {"
 692            The ˇ{ quick } brown
 693            fox jumps over
 694            the lazy dog."},
 695            Mode::Normal,
 696        );
 697
 698        // test add surrounds not with around
 699        cx.set_state(
 700            indoc! {"
 701            The quˇick brown
 702            fox jumps over
 703            the lazy dog."},
 704            Mode::Normal,
 705        );
 706        cx.simulate_keystrokes("y s i w }");
 707        cx.assert_state(
 708            indoc! {"
 709            The ˇ{quick} brown
 710            fox jumps over
 711            the lazy dog."},
 712            Mode::Normal,
 713        );
 714
 715        // test add surrounds with motion
 716        cx.set_state(
 717            indoc! {"
 718            The quˇick brown
 719            fox jumps over
 720            the lazy dog."},
 721            Mode::Normal,
 722        );
 723        cx.simulate_keystrokes("y s $ }");
 724        cx.assert_state(
 725            indoc! {"
 726            The quˇ{ick brown}
 727            fox jumps over
 728            the lazy dog."},
 729            Mode::Normal,
 730        );
 731
 732        // test add surrounds with multi cursor
 733        cx.set_state(
 734            indoc! {"
 735            The quˇick brown
 736            fox jumps over
 737            the laˇzy dog."},
 738            Mode::Normal,
 739        );
 740        cx.simulate_keystrokes("y s i w '");
 741        cx.assert_state(
 742            indoc! {"
 743            The ˇ'quick' brown
 744            fox jumps over
 745            the ˇ'lazy' dog."},
 746            Mode::Normal,
 747        );
 748
 749        // test multi cursor add surrounds with motion
 750        cx.set_state(
 751            indoc! {"
 752            The quˇick brown
 753            fox jumps over
 754            the laˇzy dog."},
 755            Mode::Normal,
 756        );
 757        cx.simulate_keystrokes("y s $ '");
 758        cx.assert_state(
 759            indoc! {"
 760            The quˇ'ick brown'
 761            fox jumps over
 762            the laˇ'zy dog.'"},
 763            Mode::Normal,
 764        );
 765
 766        // test multi cursor add surrounds with motion and custom string
 767        cx.set_state(
 768            indoc! {"
 769            The quˇick brown
 770            fox jumps over
 771            the laˇzy dog."},
 772            Mode::Normal,
 773        );
 774        cx.simulate_keystrokes("y s $ 1");
 775        cx.assert_state(
 776            indoc! {"
 777            The quˇ1ick brown1
 778            fox jumps over
 779            the laˇ1zy dog.1"},
 780            Mode::Normal,
 781        );
 782
 783        // test add surrounds with motion current line
 784        cx.set_state(
 785            indoc! {"
 786            The quˇick brown
 787            fox jumps over
 788            the lazy dog."},
 789            Mode::Normal,
 790        );
 791        cx.simulate_keystrokes("y s s {");
 792        cx.assert_state(
 793            indoc! {"
 794            ˇ{ The quick brown }
 795            fox jumps over
 796            the lazy dog."},
 797            Mode::Normal,
 798        );
 799
 800        cx.set_state(
 801            indoc! {"
 802                The quˇick brown•
 803            fox jumps over
 804            the lazy dog."},
 805            Mode::Normal,
 806        );
 807        cx.simulate_keystrokes("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        cx.simulate_keystrokes("2 y s s )");
 816        cx.assert_state(
 817            indoc! {"
 818                ˇ({ The quick brown }•
 819            fox jumps over)
 820            the lazy dog."},
 821            Mode::Normal,
 822        );
 823
 824        // test add surrounds around object
 825        cx.set_state(
 826            indoc! {"
 827            The [quˇick] brown
 828            fox jumps over
 829            the lazy dog."},
 830            Mode::Normal,
 831        );
 832        cx.simulate_keystrokes("y s a ] )");
 833        cx.assert_state(
 834            indoc! {"
 835            The ˇ([quick]) brown
 836            fox jumps over
 837            the lazy dog."},
 838            Mode::Normal,
 839        );
 840
 841        // test add surrounds inside object
 842        cx.set_state(
 843            indoc! {"
 844            The [quˇick] brown
 845            fox jumps over
 846            the lazy dog."},
 847            Mode::Normal,
 848        );
 849        cx.simulate_keystrokes("y s i ] )");
 850        cx.assert_state(
 851            indoc! {"
 852            The [ˇ(quick)] brown
 853            fox jumps over
 854            the lazy dog."},
 855            Mode::Normal,
 856        );
 857    }
 858
 859    #[gpui::test]
 860    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 861        let mut cx = VimTestContext::new(cx, true).await;
 862
 863        cx.update(|_, cx| {
 864            cx.bind_keys([KeyBinding::new(
 865                "shift-s",
 866                PushAddSurrounds {},
 867                Some("vim_mode == visual"),
 868            )])
 869        });
 870
 871        // test add surrounds with around
 872        cx.set_state(
 873            indoc! {"
 874            The quˇick brown
 875            fox jumps over
 876            the lazy dog."},
 877            Mode::Normal,
 878        );
 879        cx.simulate_keystrokes("v i w shift-s {");
 880        cx.assert_state(
 881            indoc! {"
 882            The ˇ{ quick } brown
 883            fox jumps over
 884            the lazy dog."},
 885            Mode::Normal,
 886        );
 887
 888        // test add surrounds not with around
 889        cx.set_state(
 890            indoc! {"
 891            The quˇick brown
 892            fox jumps over
 893            the lazy dog."},
 894            Mode::Normal,
 895        );
 896        cx.simulate_keystrokes("v i w shift-s }");
 897        cx.assert_state(
 898            indoc! {"
 899            The ˇ{quick} brown
 900            fox jumps over
 901            the lazy dog."},
 902            Mode::Normal,
 903        );
 904
 905        // test add surrounds with motion
 906        cx.set_state(
 907            indoc! {"
 908            The quˇick brown
 909            fox jumps over
 910            the lazy dog."},
 911            Mode::Normal,
 912        );
 913        cx.simulate_keystrokes("v e shift-s }");
 914        cx.assert_state(
 915            indoc! {"
 916            The quˇ{ick} brown
 917            fox jumps over
 918            the lazy dog."},
 919            Mode::Normal,
 920        );
 921
 922        // test add surrounds with multi cursor
 923        cx.set_state(
 924            indoc! {"
 925            The quˇick brown
 926            fox jumps over
 927            the laˇzy dog."},
 928            Mode::Normal,
 929        );
 930        cx.simulate_keystrokes("v i w shift-s '");
 931        cx.assert_state(
 932            indoc! {"
 933            The ˇ'quick' brown
 934            fox jumps over
 935            the ˇ'lazy' dog."},
 936            Mode::Normal,
 937        );
 938
 939        // test add surrounds with visual block
 940        cx.set_state(
 941            indoc! {"
 942            The quˇick brown
 943            fox jumps over
 944            the lazy dog."},
 945            Mode::Normal,
 946        );
 947        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 948        cx.assert_state(
 949            indoc! {"
 950            The ˇ'quick' brown
 951            fox 'jumps' over
 952            the 'lazy 'dog."},
 953            Mode::Normal,
 954        );
 955
 956        // test add surrounds with visual line
 957        cx.set_state(
 958            indoc! {"
 959            The quˇick brown
 960            fox jumps over
 961            the lazy dog."},
 962            Mode::Normal,
 963        );
 964        cx.simulate_keystrokes("j shift-v shift-s '");
 965        cx.assert_state(
 966            indoc! {"
 967            The quick brown
 968            ˇ'
 969            fox jumps over
 970            '
 971            the lazy dog."},
 972            Mode::Normal,
 973        );
 974    }
 975
 976    #[gpui::test]
 977    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 978        let mut cx = VimTestContext::new(cx, true).await;
 979
 980        // test delete surround
 981        cx.set_state(
 982            indoc! {"
 983            The {quˇick} brown
 984            fox jumps over
 985            the lazy dog."},
 986            Mode::Normal,
 987        );
 988        cx.simulate_keystrokes("d s {");
 989        cx.assert_state(
 990            indoc! {"
 991            The ˇquick brown
 992            fox jumps over
 993            the lazy dog."},
 994            Mode::Normal,
 995        );
 996
 997        // test delete not exist surrounds
 998        cx.set_state(
 999            indoc! {"
1000            The {quˇick} brown
1001            fox jumps over
1002            the lazy dog."},
1003            Mode::Normal,
1004        );
1005        cx.simulate_keystrokes("d s [");
1006        cx.assert_state(
1007            indoc! {"
1008            The {quˇick} brown
1009            fox jumps over
1010            the lazy dog."},
1011            Mode::Normal,
1012        );
1013
1014        // test delete surround forward exist, in the surrounds plugin of other editors,
1015        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
1016        cx.set_state(
1017            indoc! {"
1018            The {quick} brˇown
1019            fox jumps over
1020            the lazy dog."},
1021            Mode::Normal,
1022        );
1023        cx.simulate_keystrokes("d s {");
1024        cx.assert_state(
1025            indoc! {"
1026            The {quick} brˇown
1027            fox jumps over
1028            the lazy dog."},
1029            Mode::Normal,
1030        );
1031
1032        // test cursor delete inner surrounds
1033        cx.set_state(
1034            indoc! {"
1035            The { quick brown
1036            fox jumˇps over }
1037            the lazy dog."},
1038            Mode::Normal,
1039        );
1040        cx.simulate_keystrokes("d s {");
1041        cx.assert_state(
1042            indoc! {"
1043            The ˇquick brown
1044            fox jumps over
1045            the lazy dog."},
1046            Mode::Normal,
1047        );
1048
1049        // test multi cursor delete surrounds
1050        cx.set_state(
1051            indoc! {"
1052            The [quˇick] brown
1053            fox jumps over
1054            the [laˇzy] dog."},
1055            Mode::Normal,
1056        );
1057        cx.simulate_keystrokes("d s ]");
1058        cx.assert_state(
1059            indoc! {"
1060            The ˇquick brown
1061            fox jumps over
1062            the ˇlazy dog."},
1063            Mode::Normal,
1064        );
1065
1066        // test multi cursor delete surrounds with around
1067        cx.set_state(
1068            indoc! {"
1069            Tˇhe [ quick ] brown
1070            fox jumps over
1071            the [laˇzy] dog."},
1072            Mode::Normal,
1073        );
1074        cx.simulate_keystrokes("d s [");
1075        cx.assert_state(
1076            indoc! {"
1077            The ˇquick brown
1078            fox jumps over
1079            the ˇlazy dog."},
1080            Mode::Normal,
1081        );
1082
1083        cx.set_state(
1084            indoc! {"
1085            Tˇhe [ quick ] brown
1086            fox jumps over
1087            the [laˇzy ] dog."},
1088            Mode::Normal,
1089        );
1090        cx.simulate_keystrokes("d s [");
1091        cx.assert_state(
1092            indoc! {"
1093            The ˇquick brown
1094            fox jumps over
1095            the ˇlazy dog."},
1096            Mode::Normal,
1097        );
1098
1099        // test multi cursor delete different surrounds
1100        // the pair corresponding to the two cursors is the same,
1101        // so they are combined into one cursor
1102        cx.set_state(
1103            indoc! {"
1104            The [quˇick] brown
1105            fox jumps over
1106            the {laˇzy} dog."},
1107            Mode::Normal,
1108        );
1109        cx.simulate_keystrokes("d s {");
1110        cx.assert_state(
1111            indoc! {"
1112            The [quick] brown
1113            fox jumps over
1114            the ˇlazy dog."},
1115            Mode::Normal,
1116        );
1117
1118        // test delete surround with multi cursor and nest surrounds
1119        cx.set_state(
1120            indoc! {"
1121            fn test_surround() {
1122                ifˇ 2 > 1 {
1123                    ˇprintln!(\"it is fine\");
1124                };
1125            }"},
1126            Mode::Normal,
1127        );
1128        cx.simulate_keystrokes("d s }");
1129        cx.assert_state(
1130            indoc! {"
1131            fn test_surround() ˇ
1132                if 2 > 1 ˇ
1133                    println!(\"it is fine\");
1134                ;
1135            "},
1136            Mode::Normal,
1137        );
1138    }
1139
1140    #[gpui::test]
1141    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1142        let mut cx = VimTestContext::new(cx, true).await;
1143
1144        cx.set_state(
1145            indoc! {"
1146            The {quˇick} brown
1147            fox jumps over
1148            the lazy dog."},
1149            Mode::Normal,
1150        );
1151        cx.simulate_keystrokes("c s { [");
1152        cx.assert_state(
1153            indoc! {"
1154            The ˇ[ quick ] brown
1155            fox jumps over
1156            the lazy dog."},
1157            Mode::Normal,
1158        );
1159
1160        // test multi cursor change surrounds
1161        cx.set_state(
1162            indoc! {"
1163            The {quˇick} brown
1164            fox jumps over
1165            the {laˇzy} dog."},
1166            Mode::Normal,
1167        );
1168        cx.simulate_keystrokes("c s { [");
1169        cx.assert_state(
1170            indoc! {"
1171            The ˇ[ quick ] brown
1172            fox jumps over
1173            the ˇ[ lazy ] dog."},
1174            Mode::Normal,
1175        );
1176
1177        // test multi cursor delete different surrounds with after cursor
1178        cx.set_state(
1179            indoc! {"
1180            Thˇe {quick} brown
1181            fox jumps over
1182            the {laˇzy} dog."},
1183            Mode::Normal,
1184        );
1185        cx.simulate_keystrokes("c s { [");
1186        cx.assert_state(
1187            indoc! {"
1188            The ˇ[ quick ] brown
1189            fox jumps over
1190            the ˇ[ lazy ] dog."},
1191            Mode::Normal,
1192        );
1193
1194        // test multi cursor change surrount with not around
1195        cx.set_state(
1196            indoc! {"
1197            Thˇe { quick } brown
1198            fox jumps over
1199            the {laˇzy} dog."},
1200            Mode::Normal,
1201        );
1202        cx.simulate_keystrokes("c s { ]");
1203        cx.assert_state(
1204            indoc! {"
1205            The ˇ[quick] brown
1206            fox jumps over
1207            the ˇ[lazy] dog."},
1208            Mode::Normal,
1209        );
1210
1211        // test multi cursor change with not exist surround
1212        cx.set_state(
1213            indoc! {"
1214            The {quˇick} brown
1215            fox jumps over
1216            the [laˇzy] dog."},
1217            Mode::Normal,
1218        );
1219        cx.simulate_keystrokes("c s [ '");
1220        cx.assert_state(
1221            indoc! {"
1222            The {quick} brown
1223            fox jumps over
1224            the ˇ'lazy' dog."},
1225            Mode::Normal,
1226        );
1227
1228        // test change nesting surrounds
1229        cx.set_state(
1230            indoc! {"
1231            fn test_surround() {
1232                ifˇ 2 > 1 {
1233                    ˇprintln!(\"it is fine\");
1234                }
1235            };"},
1236            Mode::Normal,
1237        );
1238        cx.simulate_keystrokes("c s } ]");
1239        cx.assert_state(
1240            indoc! {"
1241            fn test_surround() ˇ[
1242                if 2 > 1 ˇ[
1243                    println!(\"it is fine\");
1244                ]
1245            ];"},
1246            Mode::Normal,
1247        );
1248
1249        // Currently, the same test case but using the closing bracket `]`
1250        // actually removes a whitespace before the closing bracket, something
1251        // that might need to be fixed?
1252        cx.set_state(
1253            indoc! {"
1254            fn test_surround() {
1255                ifˇ 2 > 1 {
1256                    ˇprintln!(\"it is fine\");
1257                }
1258            };"},
1259            Mode::Normal,
1260        );
1261        cx.simulate_keystrokes("c s { ]");
1262        cx.assert_state(
1263            indoc! {"
1264            fn test_surround() ˇ[
1265                if 2 > 1 ˇ[
1266                    println!(\"it is fine\");
1267                ]
1268            ];"},
1269            Mode::Normal,
1270        );
1271
1272        // test change quotes.
1273        cx.set_state(indoc! {"'  ˇstr  '"}, Mode::Normal);
1274        cx.simulate_keystrokes("c s ' \"");
1275        cx.assert_state(indoc! {"ˇ\"  str  \""}, Mode::Normal);
1276
1277        // test multi cursor change quotes
1278        cx.set_state(
1279            indoc! {"
1280            '  ˇstr  '
1281            some example text here
1282            ˇ'  str  '
1283        "},
1284            Mode::Normal,
1285        );
1286        cx.simulate_keystrokes("c s ' \"");
1287        cx.assert_state(
1288            indoc! {"
1289            ˇ\"  str  \"
1290            some example text here
1291            ˇ\"  str  \"
1292        "},
1293            Mode::Normal,
1294        );
1295
1296        // test quote to bracket spacing.
1297        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1298        cx.simulate_keystrokes("c s ' {");
1299        cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1300
1301        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1302        cx.simulate_keystrokes("c s ' }");
1303        cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1304    }
1305
1306    #[gpui::test]
1307    async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1308        let mut cx = VimTestContext::new(cx, true).await;
1309
1310        // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1311        // action.
1312        cx.update(|_, cx| {
1313            cx.bind_keys([KeyBinding::new(
1314                "b",
1315                AnyBrackets,
1316                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1317            )]);
1318        });
1319
1320        cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1321        cx.simulate_keystrokes("c s b [");
1322        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1323
1324        cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1325        cx.simulate_keystrokes("c s b {");
1326        cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1327
1328        cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1329        cx.simulate_keystrokes("c s b [");
1330        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1331
1332        cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1333        cx.simulate_keystrokes("c s b [");
1334        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1335
1336        cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1337        cx.simulate_keystrokes("c s b }");
1338        cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1339
1340        cx.set_state(
1341            indoc! {"
1342            (< name: ˇ'Zed' >)
1343            (< nˇame: 'DeltaDB' >)
1344        "},
1345            Mode::Normal,
1346        );
1347        cx.simulate_keystrokes("c s b {");
1348        cx.set_state(
1349            indoc! {"
1350            (ˇ{ name: 'Zed' })
1351            (ˇ{ name: 'DeltaDB' })
1352        "},
1353            Mode::Normal,
1354        );
1355    }
1356
1357    // The following test cases all follow tpope/vim-surround's behaviour
1358    // and are more focused on how whitespace is handled.
1359    #[gpui::test]
1360    async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
1361        let mut cx = VimTestContext::new(cx, true).await;
1362
1363        // Changing quote to quote should never change the surrounding
1364        // whitespace.
1365        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1366        cx.simulate_keystrokes("c s ' \"");
1367        cx.assert_state(indoc! {"ˇ\"  a  \""}, Mode::Normal);
1368
1369        cx.set_state(indoc! {"\"  ˇa  \""}, Mode::Normal);
1370        cx.simulate_keystrokes("c s \" '");
1371        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
1372
1373        // Changing quote to bracket adds one more space when the opening
1374        // bracket is used, does not affect whitespace when the closing bracket
1375        // is used.
1376        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1377        cx.simulate_keystrokes("c s ' {");
1378        cx.assert_state(indoc! {"ˇ{   a   }"}, Mode::Normal);
1379
1380        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
1381        cx.simulate_keystrokes("c s ' }");
1382        cx.assert_state(indoc! {"ˇ{  a  }"}, Mode::Normal);
1383
1384        // Changing bracket to quote should remove all space when the
1385        // opening bracket is used and preserve all space when the
1386        // closing one is used.
1387        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1388        cx.simulate_keystrokes("c s { '");
1389        cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
1390
1391        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1392        cx.simulate_keystrokes("c s } '");
1393        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
1394
1395        // Changing bracket to bracket follows these rules:
1396        // * opening → opening – keeps only one space.
1397        // * opening → closing – removes all space.
1398        // * closing → opening – adds one space.
1399        // * closing → closing – does not change space.
1400        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
1401        cx.simulate_keystrokes("c s { [");
1402        cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1403
1404        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
1405        cx.simulate_keystrokes("c s { ]");
1406        cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
1407
1408        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1409        cx.simulate_keystrokes("c s } [");
1410        cx.assert_state(indoc! {"ˇ[   a   ]"}, Mode::Normal);
1411
1412        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
1413        cx.simulate_keystrokes("c s } ]");
1414        cx.assert_state(indoc! {"ˇ[  a  ]"}, Mode::Normal);
1415    }
1416
1417    #[gpui::test]
1418    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1419        let mut cx = VimTestContext::new(cx, true).await;
1420
1421        cx.set_state(
1422            indoc! {"
1423            The quˇick brown
1424            fox jumps over
1425            the lazy dog."},
1426            Mode::Normal,
1427        );
1428        cx.simulate_keystrokes("y s i w [");
1429        cx.assert_state(
1430            indoc! {"
1431            The ˇ[ quick ] brown
1432            fox jumps over
1433            the lazy dog."},
1434            Mode::Normal,
1435        );
1436
1437        cx.simulate_keystrokes("c s [ }");
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("d 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("u");
1456        cx.assert_state(
1457            indoc! {"
1458            The ˇ{quick} brown
1459            fox jumps over
1460            the lazy dog."},
1461            Mode::Normal,
1462        );
1463    }
1464
1465    #[gpui::test]
1466    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1467        let mut cx = VimTestContext::new(cx, true).await;
1468
1469        // add aliases
1470        cx.set_state(
1471            indoc! {"
1472            The quˇick brown
1473            fox jumps over
1474            the lazy dog."},
1475            Mode::Normal,
1476        );
1477        cx.simulate_keystrokes("y s i w b");
1478        cx.assert_state(
1479            indoc! {"
1480            The ˇ(quick) brown
1481            fox jumps over
1482            the lazy dog."},
1483            Mode::Normal,
1484        );
1485
1486        cx.set_state(
1487            indoc! {"
1488            The quˇick brown
1489            fox jumps over
1490            the lazy dog."},
1491            Mode::Normal,
1492        );
1493        cx.simulate_keystrokes("y s i w B");
1494        cx.assert_state(
1495            indoc! {"
1496            The ˇ{quick} brown
1497            fox jumps over
1498            the lazy dog."},
1499            Mode::Normal,
1500        );
1501
1502        cx.set_state(
1503            indoc! {"
1504            The quˇick brown
1505            fox jumps over
1506            the lazy dog."},
1507            Mode::Normal,
1508        );
1509        cx.simulate_keystrokes("y s i w a");
1510        cx.assert_state(
1511            indoc! {"
1512            The ˇ<quick> brown
1513            fox jumps over
1514            the lazy dog."},
1515            Mode::Normal,
1516        );
1517
1518        cx.set_state(
1519            indoc! {"
1520            The quˇick brown
1521            fox jumps over
1522            the lazy dog."},
1523            Mode::Normal,
1524        );
1525        cx.simulate_keystrokes("y s i w r");
1526        cx.assert_state(
1527            indoc! {"
1528            The ˇ[quick] brown
1529            fox jumps over
1530            the lazy dog."},
1531            Mode::Normal,
1532        );
1533
1534        // change aliases
1535        cx.set_state(
1536            indoc! {"
1537            The {quˇick} brown
1538            fox jumps over
1539            the lazy dog."},
1540            Mode::Normal,
1541        );
1542        cx.simulate_keystrokes("c s { b");
1543        cx.assert_state(
1544            indoc! {"
1545            The ˇ(quick) brown
1546            fox jumps over
1547            the lazy dog."},
1548            Mode::Normal,
1549        );
1550
1551        cx.set_state(
1552            indoc! {"
1553            The (quˇick) brown
1554            fox jumps over
1555            the lazy dog."},
1556            Mode::Normal,
1557        );
1558        cx.simulate_keystrokes("c s ( B");
1559        cx.assert_state(
1560            indoc! {"
1561            The ˇ{quick} brown
1562            fox jumps over
1563            the lazy dog."},
1564            Mode::Normal,
1565        );
1566
1567        cx.set_state(
1568            indoc! {"
1569            The (quˇick) brown
1570            fox jumps over
1571            the lazy dog."},
1572            Mode::Normal,
1573        );
1574        cx.simulate_keystrokes("c s ( a");
1575        cx.assert_state(
1576            indoc! {"
1577            The ˇ<quick> brown
1578            fox jumps over
1579            the lazy dog."},
1580            Mode::Normal,
1581        );
1582
1583        cx.set_state(
1584            indoc! {"
1585            The <quˇick> brown
1586            fox jumps over
1587            the lazy dog."},
1588            Mode::Normal,
1589        );
1590        cx.simulate_keystrokes("c s < b");
1591        cx.assert_state(
1592            indoc! {"
1593            The ˇ(quick) brown
1594            fox jumps over
1595            the lazy dog."},
1596            Mode::Normal,
1597        );
1598
1599        cx.set_state(
1600            indoc! {"
1601            The (quˇick) brown
1602            fox jumps over
1603            the lazy dog."},
1604            Mode::Normal,
1605        );
1606        cx.simulate_keystrokes("c s ( r");
1607        cx.assert_state(
1608            indoc! {"
1609            The ˇ[quick] brown
1610            fox jumps over
1611            the lazy dog."},
1612            Mode::Normal,
1613        );
1614
1615        cx.set_state(
1616            indoc! {"
1617            The [quˇick] brown
1618            fox jumps over
1619            the lazy dog."},
1620            Mode::Normal,
1621        );
1622        cx.simulate_keystrokes("c s [ b");
1623        cx.assert_state(
1624            indoc! {"
1625            The ˇ(quick) brown
1626            fox jumps over
1627            the lazy dog."},
1628            Mode::Normal,
1629        );
1630
1631        // delete alias
1632        cx.set_state(
1633            indoc! {"
1634            The {quˇick} brown
1635            fox jumps over
1636            the lazy dog."},
1637            Mode::Normal,
1638        );
1639        cx.simulate_keystrokes("d s B");
1640        cx.assert_state(
1641            indoc! {"
1642            The ˇquick brown
1643            fox jumps over
1644            the lazy dog."},
1645            Mode::Normal,
1646        );
1647
1648        cx.set_state(
1649            indoc! {"
1650            The (quˇick) brown
1651            fox jumps over
1652            the lazy dog."},
1653            Mode::Normal,
1654        );
1655        cx.simulate_keystrokes("d s b");
1656        cx.assert_state(
1657            indoc! {"
1658            The ˇquick brown
1659            fox jumps over
1660            the lazy dog."},
1661            Mode::Normal,
1662        );
1663
1664        cx.set_state(
1665            indoc! {"
1666            The [quˇick] brown
1667            fox jumps over
1668            the lazy dog."},
1669            Mode::Normal,
1670        );
1671        cx.simulate_keystrokes("d s r");
1672        cx.assert_state(
1673            indoc! {"
1674            The ˇquick brown
1675            fox jumps over
1676            the lazy dog."},
1677            Mode::Normal,
1678        );
1679
1680        cx.set_state(
1681            indoc! {"
1682            The <quˇick> brown
1683            fox jumps over
1684            the lazy dog."},
1685            Mode::Normal,
1686        );
1687        cx.simulate_keystrokes("d s a");
1688        cx.assert_state(
1689            indoc! {"
1690            The ˇquick brown
1691            fox jumps over
1692            the lazy dog."},
1693            Mode::Normal,
1694        );
1695    }
1696}