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, bool),
  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, around) => {
  63                            object.range(&display_map, selection.clone(), *around)
  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        // test add surrounds around object
 702        cx.set_state(
 703            indoc! {"
 704            The [quˇick] brown
 705            fox jumps over
 706            the lazy dog."},
 707            Mode::Normal,
 708        );
 709        cx.simulate_keystrokes("y s a ] )");
 710        cx.assert_state(
 711            indoc! {"
 712            The ˇ([quick]) brown
 713            fox jumps over
 714            the lazy dog."},
 715            Mode::Normal,
 716        );
 717
 718        // test add surrounds inside object
 719        cx.set_state(
 720            indoc! {"
 721            The [quˇick] brown
 722            fox jumps over
 723            the lazy dog."},
 724            Mode::Normal,
 725        );
 726        cx.simulate_keystrokes("y s i ] )");
 727        cx.assert_state(
 728            indoc! {"
 729            The [ˇ(quick)] brown
 730            fox jumps over
 731            the lazy dog."},
 732            Mode::Normal,
 733        );
 734    }
 735
 736    #[gpui::test]
 737    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
 738        let mut cx = VimTestContext::new(cx, true).await;
 739
 740        cx.update(|cx| {
 741            cx.bind_keys([KeyBinding::new(
 742                "shift-s",
 743                PushOperator(Operator::AddSurrounds { target: None }),
 744                Some("vim_mode == visual"),
 745            )])
 746        });
 747
 748        // test add surrounds with around
 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 i w shift-s {");
 757        cx.assert_state(
 758            indoc! {"
 759            The ˇ{ quick } brown
 760            fox jumps over
 761            the lazy dog."},
 762            Mode::Normal,
 763        );
 764
 765        // test add surrounds not with around
 766        cx.set_state(
 767            indoc! {"
 768            The quˇick brown
 769            fox jumps over
 770            the lazy 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 motion
 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("v e shift-s }");
 791        cx.assert_state(
 792            indoc! {"
 793            The quˇ{ick} brown
 794            fox jumps over
 795            the lazy dog."},
 796            Mode::Normal,
 797        );
 798
 799        // test add surrounds with multi cursor
 800        cx.set_state(
 801            indoc! {"
 802            The quˇick brown
 803            fox jumps over
 804            the laˇzy dog."},
 805            Mode::Normal,
 806        );
 807        cx.simulate_keystrokes("v i w shift-s '");
 808        cx.assert_state(
 809            indoc! {"
 810            The ˇ'quick' brown
 811            fox jumps over
 812            the ˇ'lazy' dog."},
 813            Mode::Normal,
 814        );
 815
 816        // test add surrounds with visual block
 817        cx.set_state(
 818            indoc! {"
 819            The quˇick brown
 820            fox jumps over
 821            the lazy dog."},
 822            Mode::Normal,
 823        );
 824        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
 825        cx.assert_state(
 826            indoc! {"
 827            The ˇ'quick' brown
 828            fox 'jumps' over
 829            the 'lazy 'dog."},
 830            Mode::Normal,
 831        );
 832
 833        // test add surrounds with visual line
 834        cx.set_state(
 835            indoc! {"
 836            The quˇick brown
 837            fox jumps over
 838            the lazy dog."},
 839            Mode::Normal,
 840        );
 841        cx.simulate_keystrokes("j shift-v shift-s '");
 842        cx.assert_state(
 843            indoc! {"
 844            The quick brown
 845            ˇ'
 846            fox jumps over
 847            '
 848            the lazy dog."},
 849            Mode::Normal,
 850        );
 851    }
 852
 853    #[gpui::test]
 854    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
 855        let mut cx = VimTestContext::new(cx, true).await;
 856
 857        // test delete surround
 858        cx.set_state(
 859            indoc! {"
 860            The {quˇick} brown
 861            fox jumps over
 862            the lazy dog."},
 863            Mode::Normal,
 864        );
 865        cx.simulate_keystrokes("d s {");
 866        cx.assert_state(
 867            indoc! {"
 868            The ˇquick brown
 869            fox jumps over
 870            the lazy dog."},
 871            Mode::Normal,
 872        );
 873
 874        // test delete not exist surrounds
 875        cx.set_state(
 876            indoc! {"
 877            The {quˇick} brown
 878            fox jumps over
 879            the lazy dog."},
 880            Mode::Normal,
 881        );
 882        cx.simulate_keystrokes("d s [");
 883        cx.assert_state(
 884            indoc! {"
 885            The {quˇick} brown
 886            fox jumps over
 887            the lazy dog."},
 888            Mode::Normal,
 889        );
 890
 891        // test delete surround forward exist, in the surrounds plugin of other editors,
 892        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
 893        cx.set_state(
 894            indoc! {"
 895            The {quick} brˇown
 896            fox jumps over
 897            the lazy dog."},
 898            Mode::Normal,
 899        );
 900        cx.simulate_keystrokes("d s {");
 901        cx.assert_state(
 902            indoc! {"
 903            The {quick} brˇown
 904            fox jumps over
 905            the lazy dog."},
 906            Mode::Normal,
 907        );
 908
 909        // test cursor delete inner surrounds
 910        cx.set_state(
 911            indoc! {"
 912            The { quick brown
 913            fox jumˇps over }
 914            the lazy 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        // test multi cursor delete surrounds
 927        cx.set_state(
 928            indoc! {"
 929            The [quˇick] brown
 930            fox jumps over
 931            the [laˇzy] dog."},
 932            Mode::Normal,
 933        );
 934        cx.simulate_keystrokes("d s ]");
 935        cx.assert_state(
 936            indoc! {"
 937            The ˇquick brown
 938            fox jumps over
 939            the ˇlazy dog."},
 940            Mode::Normal,
 941        );
 942
 943        // test multi cursor delete surrounds with around
 944        cx.set_state(
 945            indoc! {"
 946            Tˇhe [ quick ] brown
 947            fox jumps over
 948            the [laˇzy] dog."},
 949            Mode::Normal,
 950        );
 951        cx.simulate_keystrokes("d s [");
 952        cx.assert_state(
 953            indoc! {"
 954            The ˇquick brown
 955            fox jumps over
 956            the ˇlazy dog."},
 957            Mode::Normal,
 958        );
 959
 960        cx.set_state(
 961            indoc! {"
 962            Tˇhe [ quick ] brown
 963            fox jumps over
 964            the [laˇzy ] dog."},
 965            Mode::Normal,
 966        );
 967        cx.simulate_keystrokes("d s [");
 968        cx.assert_state(
 969            indoc! {"
 970            The ˇquick brown
 971            fox jumps over
 972            the ˇlazy dog."},
 973            Mode::Normal,
 974        );
 975
 976        // test multi cursor delete different surrounds
 977        // the pair corresponding to the two cursors is the same,
 978        // so they are combined into one cursor
 979        cx.set_state(
 980            indoc! {"
 981            The [quˇick] brown
 982            fox jumps over
 983            the {laˇzy} dog."},
 984            Mode::Normal,
 985        );
 986        cx.simulate_keystrokes("d s {");
 987        cx.assert_state(
 988            indoc! {"
 989            The [quick] brown
 990            fox jumps over
 991            the ˇlazy dog."},
 992            Mode::Normal,
 993        );
 994
 995        // test delete surround with multi cursor and nest surrounds
 996        cx.set_state(
 997            indoc! {"
 998            fn test_surround() {
 999                ifˇ 2 > 1 {
1000                    ˇprintln!(\"it is fine\");
1001                };
1002            }"},
1003            Mode::Normal,
1004        );
1005        cx.simulate_keystrokes("d s }");
1006        cx.assert_state(
1007            indoc! {"
1008            fn test_surround() ˇ
1009                if 2 > 1 ˇ
1010                    println!(\"it is fine\");
1011                ;
1012            "},
1013            Mode::Normal,
1014        );
1015    }
1016
1017    #[gpui::test]
1018    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1019        let mut cx = VimTestContext::new(cx, true).await;
1020
1021        cx.set_state(
1022            indoc! {"
1023            The {quˇick} brown
1024            fox jumps over
1025            the lazy 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 surrounds
1038        cx.set_state(
1039            indoc! {"
1040            The {quˇick} 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 delete different surrounds with after cursor
1055        cx.set_state(
1056            indoc! {"
1057            Thˇe {quick} 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 multi cursor change surrount with not around
1072        cx.set_state(
1073            indoc! {"
1074            Thˇe { quick } brown
1075            fox jumps over
1076            the {laˇzy} dog."},
1077            Mode::Normal,
1078        );
1079        cx.simulate_keystrokes("c s { ]");
1080        cx.assert_state(
1081            indoc! {"
1082            The ˇ[quick] brown
1083            fox jumps over
1084            the ˇ[lazy] dog."},
1085            Mode::Normal,
1086        );
1087
1088        // test multi cursor change with not exist surround
1089        cx.set_state(
1090            indoc! {"
1091            The {quˇick} brown
1092            fox jumps over
1093            the [laˇzy] dog."},
1094            Mode::Normal,
1095        );
1096        cx.simulate_keystrokes("c s [ '");
1097        cx.assert_state(
1098            indoc! {"
1099            The {quick} brown
1100            fox jumps over
1101            the ˇ'lazy' dog."},
1102            Mode::Normal,
1103        );
1104
1105        // test change nesting surrounds
1106        cx.set_state(
1107            indoc! {"
1108            fn test_surround() {
1109                ifˇ 2 > 1 {
1110                    ˇprintln!(\"it is fine\");
1111                }
1112            };"},
1113            Mode::Normal,
1114        );
1115        cx.simulate_keystrokes("c s { [");
1116        cx.assert_state(
1117            indoc! {"
1118            fn test_surround() ˇ[
1119                if 2 > 1 ˇ[
1120                    println!(\"it is fine\");
1121                ]
1122            ];"},
1123            Mode::Normal,
1124        );
1125    }
1126
1127    #[gpui::test]
1128    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1129        let mut cx = VimTestContext::new(cx, true).await;
1130
1131        cx.set_state(
1132            indoc! {"
1133            The quˇick brown
1134            fox jumps over
1135            the lazy dog."},
1136            Mode::Normal,
1137        );
1138        cx.simulate_keystrokes("y s i w [");
1139        cx.assert_state(
1140            indoc! {"
1141            The ˇ[ quick ] brown
1142            fox jumps over
1143            the lazy dog."},
1144            Mode::Normal,
1145        );
1146
1147        cx.simulate_keystrokes("c s [ }");
1148        cx.assert_state(
1149            indoc! {"
1150            The ˇ{quick} brown
1151            fox jumps over
1152            the lazy dog."},
1153            Mode::Normal,
1154        );
1155
1156        cx.simulate_keystrokes("d s {");
1157        cx.assert_state(
1158            indoc! {"
1159            The ˇquick brown
1160            fox jumps over
1161            the lazy dog."},
1162            Mode::Normal,
1163        );
1164
1165        cx.simulate_keystrokes("u");
1166        cx.assert_state(
1167            indoc! {"
1168            The ˇ{quick} brown
1169            fox jumps over
1170            the lazy dog."},
1171            Mode::Normal,
1172        );
1173    }
1174}