surrounds.rs

   1use crate::{
   2    Vim,
   3    motion::{self, Motion},
   4    object::Object,
   5    state::Mode,
   6};
   7use editor::{Bias, movement, scroll::Autoscroll};
   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 mode = self.mode;
  31        self.update_editor(window, cx, |_, editor, window, cx| {
  32            let text_layout_details = editor.text_layout_details(window);
  33            editor.transact(window, cx, |editor, window, cx| {
  34                editor.set_clip_at_line_ends(false, cx);
  35
  36                let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
  37                    Some(pair) => pair.clone(),
  38                    None => BracketPair {
  39                        start: text.to_string(),
  40                        end: text.to_string(),
  41                        close: true,
  42                        surround: true,
  43                        newline: false,
  44                    },
  45                };
  46                let surround = pair.end != surround_alias((*text).as_ref());
  47                let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
  48                let mut edits = Vec::new();
  49                let mut anchors = Vec::new();
  50
  51                for selection in &display_selections {
  52                    let range = match &target {
  53                        SurroundsType::Object(object, around) => {
  54                            object.range(&display_map, selection.clone(), *around)
  55                        }
  56                        SurroundsType::Motion(motion) => {
  57                            motion
  58                                .range(&display_map, selection.clone(), count, &text_layout_details)
  59                                .map(|(mut range, _)| {
  60                                    // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
  61                                    if let Motion::CurrentLine = motion {
  62                                        range.start = motion::first_non_whitespace(
  63                                            &display_map,
  64                                            false,
  65                                            range.start,
  66                                        );
  67                                        range.end = movement::saturating_right(
  68                                            &display_map,
  69                                            motion::last_non_whitespace(&display_map, range.end, 1),
  70                                        );
  71                                    }
  72                                    range
  73                                })
  74                        }
  75                        SurroundsType::Selection => Some(selection.range()),
  76                    };
  77
  78                    if let Some(range) = range {
  79                        let start = range.start.to_offset(&display_map, Bias::Right);
  80                        let end = range.end.to_offset(&display_map, Bias::Left);
  81                        let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
  82                            (format!("{}\n", pair.start), format!("\n{}", pair.end))
  83                        } else {
  84                            let maybe_space = if surround { " " } else { "" };
  85                            (
  86                                format!("{}{}", pair.start, maybe_space),
  87                                format!("{}{}", maybe_space, pair.end),
  88                            )
  89                        };
  90                        let start_anchor = display_map.buffer_snapshot.anchor_before(start);
  91
  92                        edits.push((start..start, start_cursor_str));
  93                        edits.push((end..end, end_cursor_str));
  94                        anchors.push(start_anchor..start_anchor);
  95                    } else {
  96                        let start_anchor = display_map
  97                            .buffer_snapshot
  98                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
  99                        anchors.push(start_anchor..start_anchor);
 100                    }
 101                }
 102
 103                editor.edit(edits, cx);
 104                editor.set_clip_at_line_ends(true, cx);
 105                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 106                    if mode == Mode::VisualBlock {
 107                        s.select_anchor_ranges(anchors.into_iter().take(1))
 108                    } else {
 109                        s.select_anchor_ranges(anchors)
 110                    }
 111                });
 112            });
 113        });
 114        self.switch_mode(Mode::Normal, false, window, cx);
 115    }
 116
 117    pub fn delete_surrounds(
 118        &mut self,
 119        text: Arc<str>,
 120        window: &mut Window,
 121        cx: &mut Context<Self>,
 122    ) {
 123        self.stop_recording(cx);
 124
 125        // only legitimate surrounds can be removed
 126        let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 127            Some(pair) => pair.clone(),
 128            None => return,
 129        };
 130        let pair_object = match pair_to_object(&pair) {
 131            Some(pair_object) => pair_object,
 132            None => return,
 133        };
 134        let surround = pair.end != *text;
 135
 136        self.update_editor(window, cx, |_, editor, window, cx| {
 137            editor.transact(window, cx, |editor, window, cx| {
 138                editor.set_clip_at_line_ends(false, cx);
 139
 140                let (display_map, display_selections) = editor.selections.all_display(cx);
 141                let mut edits = Vec::new();
 142                let mut anchors = Vec::new();
 143
 144                for selection in &display_selections {
 145                    let start = selection.start.to_offset(&display_map, Bias::Left);
 146                    if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
 147                        // If the current parenthesis object is single-line,
 148                        // then we need to filter whether it is the current line or not
 149                        if !pair_object.is_multiline() {
 150                            let is_same_row = selection.start.row() == range.start.row()
 151                                && selection.end.row() == range.end.row();
 152                            if !is_same_row {
 153                                anchors.push(start..start);
 154                                continue;
 155                            }
 156                        }
 157                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
 158                        // hello«ˇ  "hello in a word"  »again.
 159                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
 160                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
 161                        let mut chars_and_offset = display_map
 162                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 163                            .peekable();
 164                        while let Some((ch, offset)) = chars_and_offset.next() {
 165                            if ch.to_string() == pair.start {
 166                                let start = offset;
 167                                let mut end = start + 1;
 168                                if surround {
 169                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 170                                        if next_ch.eq(&' ') {
 171                                            end += 1;
 172                                        }
 173                                    }
 174                                }
 175                                edits.push((start..end, ""));
 176                                anchors.push(start..start);
 177                                break;
 178                            }
 179                        }
 180                        let mut reverse_chars_and_offsets = display_map
 181                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
 182                            .peekable();
 183                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 184                            if ch.to_string() == pair.end {
 185                                let mut start = offset;
 186                                let end = start + 1;
 187                                if surround {
 188                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 189                                        if next_ch.eq(&' ') {
 190                                            start -= 1;
 191                                        }
 192                                    }
 193                                }
 194                                edits.push((start..end, ""));
 195                                break;
 196                            }
 197                        }
 198                    } else {
 199                        anchors.push(start..start);
 200                    }
 201                }
 202
 203                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 204                    s.select_ranges(anchors);
 205                });
 206                edits.sort_by_key(|(range, _)| range.start);
 207                editor.edit(edits, cx);
 208                editor.set_clip_at_line_ends(true, cx);
 209            });
 210        });
 211    }
 212
 213    pub fn change_surrounds(
 214        &mut self,
 215        text: Arc<str>,
 216        target: Object,
 217        window: &mut Window,
 218        cx: &mut Context<Self>,
 219    ) {
 220        if let Some(will_replace_pair) = object_to_bracket_pair(target) {
 221            self.stop_recording(cx);
 222            self.update_editor(window, cx, |_, editor, window, cx| {
 223                editor.transact(window, cx, |editor, window, cx| {
 224                    editor.set_clip_at_line_ends(false, cx);
 225
 226                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 227                        Some(pair) => pair.clone(),
 228                        None => BracketPair {
 229                            start: text.to_string(),
 230                            end: text.to_string(),
 231                            close: true,
 232                            surround: true,
 233                            newline: false,
 234                        },
 235                    };
 236                    let surround = pair.end != surround_alias((*text).as_ref());
 237                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 238                    let mut edits = Vec::new();
 239                    let mut anchors = Vec::new();
 240
 241                    for selection in &selections {
 242                        let start = selection.start.to_offset(&display_map, Bias::Left);
 243                        if let Some(range) = target.range(&display_map, selection.clone(), true) {
 244                            if !target.is_multiline() {
 245                                let is_same_row = selection.start.row() == range.start.row()
 246                                    && selection.end.row() == range.end.row();
 247                                if !is_same_row {
 248                                    anchors.push(start..start);
 249                                    continue;
 250                                }
 251                            }
 252                            let mut chars_and_offset = display_map
 253                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
 254                                .peekable();
 255                            while let Some((ch, offset)) = chars_and_offset.next() {
 256                                if ch.to_string() == will_replace_pair.start {
 257                                    let mut open_str = pair.start.clone();
 258                                    let start = offset;
 259                                    let mut end = start + 1;
 260                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
 261                                        // If the next position is already a space or line break,
 262                                        // we don't need to splice another space even under around
 263                                        if surround && !next_ch.is_whitespace() {
 264                                            open_str.push(' ');
 265                                        } else if !surround && next_ch.to_string() == " " {
 266                                            end += 1;
 267                                        }
 268                                    }
 269                                    edits.push((start..end, open_str));
 270                                    anchors.push(start..start);
 271                                    break;
 272                                }
 273                            }
 274
 275                            let mut reverse_chars_and_offsets = display_map
 276                                .reverse_buffer_chars_at(
 277                                    range.end.to_offset(&display_map, Bias::Left),
 278                                )
 279                                .peekable();
 280                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
 281                                if ch.to_string() == will_replace_pair.end {
 282                                    let mut close_str = pair.end.clone();
 283                                    let mut start = offset;
 284                                    let end = start + 1;
 285                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
 286                                        if surround && !next_ch.is_whitespace() {
 287                                            close_str.insert(0, ' ')
 288                                        } else if !surround && next_ch.to_string() == " " {
 289                                            start -= 1;
 290                                        }
 291                                    }
 292                                    edits.push((start..end, close_str));
 293                                    break;
 294                                }
 295                            }
 296                        } else {
 297                            anchors.push(start..start);
 298                        }
 299                    }
 300
 301                    let stable_anchors = editor
 302                        .selections
 303                        .disjoint_anchors()
 304                        .iter()
 305                        .map(|selection| {
 306                            let start = selection.start.bias_left(&display_map.buffer_snapshot);
 307                            start..start
 308                        })
 309                        .collect::<Vec<_>>();
 310                    edits.sort_by_key(|(range, _)| range.start);
 311                    editor.edit(edits, cx);
 312                    editor.set_clip_at_line_ends(true, cx);
 313                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 314                        s.select_anchor_ranges(stable_anchors);
 315                    });
 316                });
 317            });
 318        }
 319    }
 320
 321    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
 322    ///
 323    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
 324    /// A pair of brackets is considered valid if it is well-formed and properly closed.
 325    ///
 326    /// 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.
 327    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
 328    pub fn check_and_move_to_valid_bracket_pair(
 329        &mut self,
 330        object: Object,
 331        window: &mut Window,
 332        cx: &mut Context<Self>,
 333    ) -> bool {
 334        let mut valid = false;
 335        if let Some(pair) = object_to_bracket_pair(object) {
 336            self.update_editor(window, cx, |_, editor, window, cx| {
 337                editor.transact(window, cx, |editor, window, cx| {
 338                    editor.set_clip_at_line_ends(false, cx);
 339                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 340                    let mut anchors = Vec::new();
 341
 342                    for selection in &selections {
 343                        let start = selection.start.to_offset(&display_map, Bias::Left);
 344                        if let Some(range) = object.range(&display_map, selection.clone(), true) {
 345                            // If the current parenthesis object is single-line,
 346                            // then we need to filter whether it is the current line or not
 347                            if object.is_multiline()
 348                                || (!object.is_multiline()
 349                                    && selection.start.row() == range.start.row()
 350                                    && selection.end.row() == range.end.row())
 351                            {
 352                                valid = true;
 353                                let chars_and_offset = display_map
 354                                    .buffer_chars_at(
 355                                        range.start.to_offset(&display_map, Bias::Left),
 356                                    )
 357                                    .peekable();
 358                                for (ch, offset) in chars_and_offset {
 359                                    if ch.to_string() == pair.start {
 360                                        anchors.push(offset..offset);
 361                                        break;
 362                                    }
 363                                }
 364                            } else {
 365                                anchors.push(start..start)
 366                            }
 367                        } else {
 368                            anchors.push(start..start)
 369                        }
 370                    }
 371                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 372                        s.select_ranges(anchors);
 373                    });
 374                    editor.set_clip_at_line_ends(true, cx);
 375                });
 376            });
 377        }
 378        valid
 379    }
 380}
 381
 382fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
 383    pairs
 384        .iter()
 385        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
 386}
 387
 388fn surround_alias(ch: &str) -> &str {
 389    match ch {
 390        "b" => ")",
 391        "B" => "}",
 392        "a" => ">",
 393        "r" => "]",
 394        _ => ch,
 395    }
 396}
 397
 398fn all_support_surround_pair() -> Vec<BracketPair> {
 399    vec![
 400        BracketPair {
 401            start: "{".into(),
 402            end: "}".into(),
 403            close: true,
 404            surround: true,
 405            newline: false,
 406        },
 407        BracketPair {
 408            start: "'".into(),
 409            end: "'".into(),
 410            close: true,
 411            surround: true,
 412            newline: false,
 413        },
 414        BracketPair {
 415            start: "`".into(),
 416            end: "`".into(),
 417            close: true,
 418            surround: true,
 419            newline: false,
 420        },
 421        BracketPair {
 422            start: "\"".into(),
 423            end: "\"".into(),
 424            close: true,
 425            surround: true,
 426            newline: false,
 427        },
 428        BracketPair {
 429            start: "(".into(),
 430            end: ")".into(),
 431            close: true,
 432            surround: true,
 433            newline: false,
 434        },
 435        BracketPair {
 436            start: "|".into(),
 437            end: "|".into(),
 438            close: true,
 439            surround: true,
 440            newline: false,
 441        },
 442        BracketPair {
 443            start: "[".into(),
 444            end: "]".into(),
 445            close: true,
 446            surround: true,
 447            newline: false,
 448        },
 449        BracketPair {
 450            start: "{".into(),
 451            end: "}".into(),
 452            close: true,
 453            surround: true,
 454            newline: false,
 455        },
 456        BracketPair {
 457            start: "<".into(),
 458            end: ">".into(),
 459            close: true,
 460            surround: true,
 461            newline: false,
 462        },
 463    ]
 464}
 465
 466fn pair_to_object(pair: &BracketPair) -> Option<Object> {
 467    match pair.start.as_str() {
 468        "'" => Some(Object::Quotes),
 469        "`" => Some(Object::BackQuotes),
 470        "\"" => Some(Object::DoubleQuotes),
 471        "|" => Some(Object::VerticalBars),
 472        "(" => Some(Object::Parentheses),
 473        "[" => Some(Object::SquareBrackets),
 474        "{" => Some(Object::CurlyBrackets),
 475        "<" => Some(Object::AngleBrackets),
 476        _ => None,
 477    }
 478}
 479
 480fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
 481    match object {
 482        Object::Quotes => Some(BracketPair {
 483            start: "'".to_string(),
 484            end: "'".to_string(),
 485            close: true,
 486            surround: true,
 487            newline: false,
 488        }),
 489        Object::BackQuotes => Some(BracketPair {
 490            start: "`".to_string(),
 491            end: "`".to_string(),
 492            close: true,
 493            surround: true,
 494            newline: false,
 495        }),
 496        Object::DoubleQuotes => Some(BracketPair {
 497            start: "\"".to_string(),
 498            end: "\"".to_string(),
 499            close: true,
 500            surround: true,
 501            newline: false,
 502        }),
 503        Object::VerticalBars => Some(BracketPair {
 504            start: "|".to_string(),
 505            end: "|".to_string(),
 506            close: true,
 507            surround: true,
 508            newline: false,
 509        }),
 510        Object::Parentheses => Some(BracketPair {
 511            start: "(".to_string(),
 512            end: ")".to_string(),
 513            close: true,
 514            surround: true,
 515            newline: false,
 516        }),
 517        Object::SquareBrackets => Some(BracketPair {
 518            start: "[".to_string(),
 519            end: "]".to_string(),
 520            close: true,
 521            surround: true,
 522            newline: false,
 523        }),
 524        Object::CurlyBrackets => Some(BracketPair {
 525            start: "{".to_string(),
 526            end: "}".to_string(),
 527            close: true,
 528            surround: true,
 529            newline: false,
 530        }),
 531        Object::AngleBrackets => Some(BracketPair {
 532            start: "<".to_string(),
 533            end: ">".to_string(),
 534            close: true,
 535            surround: true,
 536            newline: false,
 537        }),
 538        _ => None,
 539    }
 540}
 541
 542#[cfg(test)]
 543mod test {
 544    use gpui::KeyBinding;
 545    use indoc::indoc;
 546
 547    use crate::{PushAddSurrounds, state::Mode, test::VimTestContext};
 548
 549    #[gpui::test]
 550    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
 551        let mut cx = VimTestContext::new(cx, true).await;
 552
 553        // test add surrounds with around
 554        cx.set_state(
 555            indoc! {"
 556            The quˇick brown
 557            fox jumps over
 558            the lazy dog."},
 559            Mode::Normal,
 560        );
 561        cx.simulate_keystrokes("y s i w {");
 562        cx.assert_state(
 563            indoc! {"
 564            The ˇ{ quick } brown
 565            fox jumps over
 566            the lazy dog."},
 567            Mode::Normal,
 568        );
 569
 570        // test add surrounds not with around
 571        cx.set_state(
 572            indoc! {"
 573            The quˇick brown
 574            fox jumps over
 575            the lazy dog."},
 576            Mode::Normal,
 577        );
 578        cx.simulate_keystrokes("y s i w }");
 579        cx.assert_state(
 580            indoc! {"
 581            The ˇ{quick} brown
 582            fox jumps over
 583            the lazy dog."},
 584            Mode::Normal,
 585        );
 586
 587        // test add surrounds with motion
 588        cx.set_state(
 589            indoc! {"
 590            The quˇick brown
 591            fox jumps over
 592            the lazy dog."},
 593            Mode::Normal,
 594        );
 595        cx.simulate_keystrokes("y s $ }");
 596        cx.assert_state(
 597            indoc! {"
 598            The quˇ{ick brown}
 599            fox jumps over
 600            the lazy dog."},
 601            Mode::Normal,
 602        );
 603
 604        // test add surrounds with multi cursor
 605        cx.set_state(
 606            indoc! {"
 607            The quˇick brown
 608            fox jumps over
 609            the laˇzy dog."},
 610            Mode::Normal,
 611        );
 612        cx.simulate_keystrokes("y s i w '");
 613        cx.assert_state(
 614            indoc! {"
 615            The ˇ'quick' brown
 616            fox jumps over
 617            the ˇ'lazy' dog."},
 618            Mode::Normal,
 619        );
 620
 621        // test multi cursor add surrounds with motion
 622        cx.set_state(
 623            indoc! {"
 624            The quˇick brown
 625            fox jumps over
 626            the laˇzy dog."},
 627            Mode::Normal,
 628        );
 629        cx.simulate_keystrokes("y s $ '");
 630        cx.assert_state(
 631            indoc! {"
 632            The quˇ'ick brown'
 633            fox jumps over
 634            the laˇ'zy dog.'"},
 635            Mode::Normal,
 636        );
 637
 638        // test multi cursor add surrounds with motion and custom string
 639        cx.set_state(
 640            indoc! {"
 641            The quˇick brown
 642            fox jumps over
 643            the laˇzy dog."},
 644            Mode::Normal,
 645        );
 646        cx.simulate_keystrokes("y s $ 1");
 647        cx.assert_state(
 648            indoc! {"
 649            The quˇ1ick brown1
 650            fox jumps over
 651            the laˇ1zy dog.1"},
 652            Mode::Normal,
 653        );
 654
 655        // test add surrounds with motion current line
 656        cx.set_state(
 657            indoc! {"
 658            The quˇick brown
 659            fox jumps over
 660            the lazy dog."},
 661            Mode::Normal,
 662        );
 663        cx.simulate_keystrokes("y s s {");
 664        cx.assert_state(
 665            indoc! {"
 666            ˇ{ The quick brown }
 667            fox jumps over
 668            the lazy dog."},
 669            Mode::Normal,
 670        );
 671
 672        cx.set_state(
 673            indoc! {"
 674                The quˇick brown•
 675            fox jumps over
 676            the lazy dog."},
 677            Mode::Normal,
 678        );
 679        cx.simulate_keystrokes("y s s {");
 680        cx.assert_state(
 681            indoc! {"
 682                ˇ{ The quick brown }•
 683            fox jumps over
 684            the lazy dog."},
 685            Mode::Normal,
 686        );
 687        cx.simulate_keystrokes("2 y s s )");
 688        cx.assert_state(
 689            indoc! {"
 690                ˇ({ The quick brown }•
 691            fox jumps over)
 692            the lazy dog."},
 693            Mode::Normal,
 694        );
 695
 696        // test add surrounds around object
 697        cx.set_state(
 698            indoc! {"
 699            The [quˇick] brown
 700            fox jumps over
 701            the lazy dog."},
 702            Mode::Normal,
 703        );
 704        cx.simulate_keystrokes("y s a ] )");
 705        cx.assert_state(
 706            indoc! {"
 707            The ˇ([quick]) brown
 708            fox jumps over
 709            the lazy dog."},
 710            Mode::Normal,
 711        );
 712
 713        // test add surrounds inside object
 714        cx.set_state(
 715            indoc! {"
 716            The [quˇick] brown
 717            fox jumps over
 718            the lazy dog."},
 719            Mode::Normal,
 720        );
 721        cx.simulate_keystrokes("y s i ] )");
 722        cx.assert_state(
 723            indoc! {"
 724            The [ˇ(quick)] brown
 725            fox jumps over
 726            the lazy dog."},
 727            Mode::Normal,
 728        );
 729    }
 730
 731    #[gpui::test]
 732    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 733        let mut cx = VimTestContext::new(cx, true).await;
 734
 735        cx.update(|_, cx| {
 736            cx.bind_keys([KeyBinding::new(
 737                "shift-s",
 738                PushAddSurrounds {},
 739                Some("vim_mode == visual"),
 740            )])
 741        });
 742
 743        // test add surrounds with around
 744        cx.set_state(
 745            indoc! {"
 746            The quˇick brown
 747            fox jumps over
 748            the lazy dog."},
 749            Mode::Normal,
 750        );
 751        cx.simulate_keystrokes("v i w shift-s {");
 752        cx.assert_state(
 753            indoc! {"
 754            The ˇ{ quick } brown
 755            fox jumps over
 756            the lazy dog."},
 757            Mode::Normal,
 758        );
 759
 760        // test add surrounds not with around
 761        cx.set_state(
 762            indoc! {"
 763            The quˇick brown
 764            fox jumps over
 765            the lazy dog."},
 766            Mode::Normal,
 767        );
 768        cx.simulate_keystrokes("v i w shift-s }");
 769        cx.assert_state(
 770            indoc! {"
 771            The ˇ{quick} brown
 772            fox jumps over
 773            the lazy dog."},
 774            Mode::Normal,
 775        );
 776
 777        // test add surrounds with motion
 778        cx.set_state(
 779            indoc! {"
 780            The quˇick brown
 781            fox jumps over
 782            the lazy dog."},
 783            Mode::Normal,
 784        );
 785        cx.simulate_keystrokes("v e shift-s }");
 786        cx.assert_state(
 787            indoc! {"
 788            The quˇ{ick} brown
 789            fox jumps over
 790            the lazy dog."},
 791            Mode::Normal,
 792        );
 793
 794        // test add surrounds with multi cursor
 795        cx.set_state(
 796            indoc! {"
 797            The quˇick brown
 798            fox jumps over
 799            the laˇzy dog."},
 800            Mode::Normal,
 801        );
 802        cx.simulate_keystrokes("v i w shift-s '");
 803        cx.assert_state(
 804            indoc! {"
 805            The ˇ'quick' brown
 806            fox jumps over
 807            the ˇ'lazy' dog."},
 808            Mode::Normal,
 809        );
 810
 811        // test add surrounds with visual block
 812        cx.set_state(
 813            indoc! {"
 814            The quˇick brown
 815            fox jumps over
 816            the lazy dog."},
 817            Mode::Normal,
 818        );
 819        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 820        cx.assert_state(
 821            indoc! {"
 822            The ˇ'quick' brown
 823            fox 'jumps' over
 824            the 'lazy 'dog."},
 825            Mode::Normal,
 826        );
 827
 828        // test add surrounds with visual line
 829        cx.set_state(
 830            indoc! {"
 831            The quˇick brown
 832            fox jumps over
 833            the lazy dog."},
 834            Mode::Normal,
 835        );
 836        cx.simulate_keystrokes("j shift-v shift-s '");
 837        cx.assert_state(
 838            indoc! {"
 839            The quick brown
 840            ˇ'
 841            fox jumps over
 842            '
 843            the lazy dog."},
 844            Mode::Normal,
 845        );
 846    }
 847
 848    #[gpui::test]
 849    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 850        let mut cx = VimTestContext::new(cx, true).await;
 851
 852        // test delete surround
 853        cx.set_state(
 854            indoc! {"
 855            The {quˇick} brown
 856            fox jumps over
 857            the lazy dog."},
 858            Mode::Normal,
 859        );
 860        cx.simulate_keystrokes("d s {");
 861        cx.assert_state(
 862            indoc! {"
 863            The ˇquick brown
 864            fox jumps over
 865            the lazy dog."},
 866            Mode::Normal,
 867        );
 868
 869        // test delete not exist surrounds
 870        cx.set_state(
 871            indoc! {"
 872            The {quˇick} brown
 873            fox jumps over
 874            the lazy dog."},
 875            Mode::Normal,
 876        );
 877        cx.simulate_keystrokes("d s [");
 878        cx.assert_state(
 879            indoc! {"
 880            The {quˇick} brown
 881            fox jumps over
 882            the lazy dog."},
 883            Mode::Normal,
 884        );
 885
 886        // test delete surround forward exist, in the surrounds plugin of other editors,
 887        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 888        cx.set_state(
 889            indoc! {"
 890            The {quick} brˇown
 891            fox jumps over
 892            the lazy dog."},
 893            Mode::Normal,
 894        );
 895        cx.simulate_keystrokes("d s {");
 896        cx.assert_state(
 897            indoc! {"
 898            The {quick} brˇown
 899            fox jumps over
 900            the lazy dog."},
 901            Mode::Normal,
 902        );
 903
 904        // test cursor delete inner surrounds
 905        cx.set_state(
 906            indoc! {"
 907            The { quick brown
 908            fox jumˇps over }
 909            the lazy dog."},
 910            Mode::Normal,
 911        );
 912        cx.simulate_keystrokes("d s {");
 913        cx.assert_state(
 914            indoc! {"
 915            The ˇquick brown
 916            fox jumps over
 917            the lazy dog."},
 918            Mode::Normal,
 919        );
 920
 921        // test multi cursor delete surrounds
 922        cx.set_state(
 923            indoc! {"
 924            The [quˇick] brown
 925            fox jumps over
 926            the [laˇzy] dog."},
 927            Mode::Normal,
 928        );
 929        cx.simulate_keystrokes("d s ]");
 930        cx.assert_state(
 931            indoc! {"
 932            The ˇquick brown
 933            fox jumps over
 934            the ˇlazy dog."},
 935            Mode::Normal,
 936        );
 937
 938        // test multi cursor delete surrounds with around
 939        cx.set_state(
 940            indoc! {"
 941            Tˇhe [ quick ] brown
 942            fox jumps over
 943            the [laˇzy] dog."},
 944            Mode::Normal,
 945        );
 946        cx.simulate_keystrokes("d s [");
 947        cx.assert_state(
 948            indoc! {"
 949            The ˇquick brown
 950            fox jumps over
 951            the ˇlazy dog."},
 952            Mode::Normal,
 953        );
 954
 955        cx.set_state(
 956            indoc! {"
 957            Tˇhe [ quick ] brown
 958            fox jumps over
 959            the [laˇzy ] dog."},
 960            Mode::Normal,
 961        );
 962        cx.simulate_keystrokes("d s [");
 963        cx.assert_state(
 964            indoc! {"
 965            The ˇquick brown
 966            fox jumps over
 967            the ˇlazy dog."},
 968            Mode::Normal,
 969        );
 970
 971        // test multi cursor delete different surrounds
 972        // the pair corresponding to the two cursors is the same,
 973        // so they are combined into one cursor
 974        cx.set_state(
 975            indoc! {"
 976            The [quˇick] brown
 977            fox jumps over
 978            the {laˇzy} dog."},
 979            Mode::Normal,
 980        );
 981        cx.simulate_keystrokes("d s {");
 982        cx.assert_state(
 983            indoc! {"
 984            The [quick] brown
 985            fox jumps over
 986            the ˇlazy dog."},
 987            Mode::Normal,
 988        );
 989
 990        // test delete surround with multi cursor and nest surrounds
 991        cx.set_state(
 992            indoc! {"
 993            fn test_surround() {
 994                ifˇ 2 > 1 {
 995                    ˇprintln!(\"it is fine\");
 996                };
 997            }"},
 998            Mode::Normal,
 999        );
1000        cx.simulate_keystrokes("d s }");
1001        cx.assert_state(
1002            indoc! {"
1003            fn test_surround() ˇ
1004                if 2 > 1 ˇ
1005                    println!(\"it is fine\");
1006                ;
1007            "},
1008            Mode::Normal,
1009        );
1010    }
1011
1012    #[gpui::test]
1013    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1014        let mut cx = VimTestContext::new(cx, true).await;
1015
1016        cx.set_state(
1017            indoc! {"
1018            The {quˇick} brown
1019            fox jumps over
1020            the lazy dog."},
1021            Mode::Normal,
1022        );
1023        cx.simulate_keystrokes("c s { [");
1024        cx.assert_state(
1025            indoc! {"
1026            The ˇ[ quick ] brown
1027            fox jumps over
1028            the lazy dog."},
1029            Mode::Normal,
1030        );
1031
1032        // test multi cursor change surrounds
1033        cx.set_state(
1034            indoc! {"
1035            The {quˇick} brown
1036            fox jumps over
1037            the {laˇzy} dog."},
1038            Mode::Normal,
1039        );
1040        cx.simulate_keystrokes("c s { [");
1041        cx.assert_state(
1042            indoc! {"
1043            The ˇ[ quick ] brown
1044            fox jumps over
1045            the ˇ[ lazy ] dog."},
1046            Mode::Normal,
1047        );
1048
1049        // test multi cursor delete different surrounds with after cursor
1050        cx.set_state(
1051            indoc! {"
1052            Thˇe {quick} brown
1053            fox jumps over
1054            the {laˇzy} dog."},
1055            Mode::Normal,
1056        );
1057        cx.simulate_keystrokes("c s { [");
1058        cx.assert_state(
1059            indoc! {"
1060            The ˇ[ quick ] brown
1061            fox jumps over
1062            the ˇ[ lazy ] dog."},
1063            Mode::Normal,
1064        );
1065
1066        // test multi cursor change surrount with not around
1067        cx.set_state(
1068            indoc! {"
1069            Thˇe { quick } brown
1070            fox jumps over
1071            the {laˇzy} dog."},
1072            Mode::Normal,
1073        );
1074        cx.simulate_keystrokes("c s { ]");
1075        cx.assert_state(
1076            indoc! {"
1077            The ˇ[quick] brown
1078            fox jumps over
1079            the ˇ[lazy] dog."},
1080            Mode::Normal,
1081        );
1082
1083        // test multi cursor change with not exist surround
1084        cx.set_state(
1085            indoc! {"
1086            The {quˇick} brown
1087            fox jumps over
1088            the [laˇzy] dog."},
1089            Mode::Normal,
1090        );
1091        cx.simulate_keystrokes("c s [ '");
1092        cx.assert_state(
1093            indoc! {"
1094            The {quick} brown
1095            fox jumps over
1096            the ˇ'lazy' dog."},
1097            Mode::Normal,
1098        );
1099
1100        // test change nesting surrounds
1101        cx.set_state(
1102            indoc! {"
1103            fn test_surround() {
1104                ifˇ 2 > 1 {
1105                    ˇprintln!(\"it is fine\");
1106                }
1107            };"},
1108            Mode::Normal,
1109        );
1110        cx.simulate_keystrokes("c s { [");
1111        cx.assert_state(
1112            indoc! {"
1113            fn test_surround() ˇ[
1114                if 2 > 1 ˇ[
1115                    println!(\"it is fine\");
1116                ]
1117            ];"},
1118            Mode::Normal,
1119        );
1120    }
1121
1122    #[gpui::test]
1123    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1124        let mut cx = VimTestContext::new(cx, true).await;
1125
1126        cx.set_state(
1127            indoc! {"
1128            The quˇick brown
1129            fox jumps over
1130            the lazy dog."},
1131            Mode::Normal,
1132        );
1133        cx.simulate_keystrokes("y s i w [");
1134        cx.assert_state(
1135            indoc! {"
1136            The ˇ[ quick ] brown
1137            fox jumps over
1138            the lazy dog."},
1139            Mode::Normal,
1140        );
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        cx.simulate_keystrokes("d s {");
1152        cx.assert_state(
1153            indoc! {"
1154            The ˇquick brown
1155            fox jumps over
1156            the lazy dog."},
1157            Mode::Normal,
1158        );
1159
1160        cx.simulate_keystrokes("u");
1161        cx.assert_state(
1162            indoc! {"
1163            The ˇ{quick} brown
1164            fox jumps over
1165            the lazy dog."},
1166            Mode::Normal,
1167        );
1168    }
1169
1170    #[gpui::test]
1171    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1172        let mut cx = VimTestContext::new(cx, true).await;
1173
1174        // add aliases
1175        cx.set_state(
1176            indoc! {"
1177            The quˇick brown
1178            fox jumps over
1179            the lazy dog."},
1180            Mode::Normal,
1181        );
1182        cx.simulate_keystrokes("y s i w b");
1183        cx.assert_state(
1184            indoc! {"
1185            The ˇ(quick) brown
1186            fox jumps over
1187            the lazy dog."},
1188            Mode::Normal,
1189        );
1190
1191        cx.set_state(
1192            indoc! {"
1193            The quˇick brown
1194            fox jumps over
1195            the lazy dog."},
1196            Mode::Normal,
1197        );
1198        cx.simulate_keystrokes("y s i w B");
1199        cx.assert_state(
1200            indoc! {"
1201            The ˇ{quick} brown
1202            fox jumps over
1203            the lazy dog."},
1204            Mode::Normal,
1205        );
1206
1207        cx.set_state(
1208            indoc! {"
1209            The quˇick brown
1210            fox jumps over
1211            the lazy dog."},
1212            Mode::Normal,
1213        );
1214        cx.simulate_keystrokes("y s i w a");
1215        cx.assert_state(
1216            indoc! {"
1217            The ˇ<quick> brown
1218            fox jumps over
1219            the lazy dog."},
1220            Mode::Normal,
1221        );
1222
1223        cx.set_state(
1224            indoc! {"
1225            The quˇick brown
1226            fox jumps over
1227            the lazy dog."},
1228            Mode::Normal,
1229        );
1230        cx.simulate_keystrokes("y s i w r");
1231        cx.assert_state(
1232            indoc! {"
1233            The ˇ[quick] brown
1234            fox jumps over
1235            the lazy dog."},
1236            Mode::Normal,
1237        );
1238
1239        // change aliases
1240        cx.set_state(
1241            indoc! {"
1242            The {quˇick} brown
1243            fox jumps over
1244            the lazy dog."},
1245            Mode::Normal,
1246        );
1247        cx.simulate_keystrokes("c s { b");
1248        cx.assert_state(
1249            indoc! {"
1250            The ˇ(quick) brown
1251            fox jumps over
1252            the lazy dog."},
1253            Mode::Normal,
1254        );
1255
1256        cx.set_state(
1257            indoc! {"
1258            The (quˇick) brown
1259            fox jumps over
1260            the lazy dog."},
1261            Mode::Normal,
1262        );
1263        cx.simulate_keystrokes("c s ( B");
1264        cx.assert_state(
1265            indoc! {"
1266            The ˇ{quick} brown
1267            fox jumps over
1268            the lazy dog."},
1269            Mode::Normal,
1270        );
1271
1272        cx.set_state(
1273            indoc! {"
1274            The (quˇick) brown
1275            fox jumps over
1276            the lazy dog."},
1277            Mode::Normal,
1278        );
1279        cx.simulate_keystrokes("c s ( a");
1280        cx.assert_state(
1281            indoc! {"
1282            The ˇ<quick> brown
1283            fox jumps over
1284            the lazy dog."},
1285            Mode::Normal,
1286        );
1287
1288        cx.set_state(
1289            indoc! {"
1290            The <quˇick> brown
1291            fox jumps over
1292            the lazy dog."},
1293            Mode::Normal,
1294        );
1295        cx.simulate_keystrokes("c s < b");
1296        cx.assert_state(
1297            indoc! {"
1298            The ˇ(quick) brown
1299            fox jumps over
1300            the lazy dog."},
1301            Mode::Normal,
1302        );
1303
1304        cx.set_state(
1305            indoc! {"
1306            The (quˇick) brown
1307            fox jumps over
1308            the lazy dog."},
1309            Mode::Normal,
1310        );
1311        cx.simulate_keystrokes("c s ( r");
1312        cx.assert_state(
1313            indoc! {"
1314            The ˇ[quick] brown
1315            fox jumps over
1316            the lazy dog."},
1317            Mode::Normal,
1318        );
1319
1320        cx.set_state(
1321            indoc! {"
1322            The [quˇick] brown
1323            fox jumps over
1324            the lazy dog."},
1325            Mode::Normal,
1326        );
1327        cx.simulate_keystrokes("c s [ b");
1328        cx.assert_state(
1329            indoc! {"
1330            The ˇ(quick) brown
1331            fox jumps over
1332            the lazy dog."},
1333            Mode::Normal,
1334        );
1335
1336        // delete alias
1337        cx.set_state(
1338            indoc! {"
1339            The {quˇick} brown
1340            fox jumps over
1341            the lazy dog."},
1342            Mode::Normal,
1343        );
1344        cx.simulate_keystrokes("d s B");
1345        cx.assert_state(
1346            indoc! {"
1347            The ˇquick brown
1348            fox jumps over
1349            the lazy dog."},
1350            Mode::Normal,
1351        );
1352
1353        cx.set_state(
1354            indoc! {"
1355            The (quˇick) brown
1356            fox jumps over
1357            the lazy dog."},
1358            Mode::Normal,
1359        );
1360        cx.simulate_keystrokes("d s b");
1361        cx.assert_state(
1362            indoc! {"
1363            The ˇquick brown
1364            fox jumps over
1365            the lazy dog."},
1366            Mode::Normal,
1367        );
1368
1369        cx.set_state(
1370            indoc! {"
1371            The [quˇick] brown
1372            fox jumps over
1373            the lazy dog."},
1374            Mode::Normal,
1375        );
1376        cx.simulate_keystrokes("d s r");
1377        cx.assert_state(
1378            indoc! {"
1379            The ˇquick brown
1380            fox jumps over
1381            the lazy dog."},
1382            Mode::Normal,
1383        );
1384
1385        cx.set_state(
1386            indoc! {"
1387            The <quˇick> brown
1388            fox jumps over
1389            the lazy dog."},
1390            Mode::Normal,
1391        );
1392        cx.simulate_keystrokes("d s a");
1393        cx.assert_state(
1394            indoc! {"
1395            The ˇquick brown
1396            fox jumps over
1397            the lazy dog."},
1398            Mode::Normal,
1399        );
1400    }
1401}