surrounds.rs

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