surrounds.rs

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