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