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