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