object.rs

   1use std::ops::Range;
   2
   3use editor::{
   4    char_kind,
   5    display_map::{DisplaySnapshot, ToDisplayPoint},
   6    movement::{self, FindRange},
   7    Bias, CharKind, DisplayPoint,
   8};
   9use gpui::{actions, Action, AppContext, WindowContext};
  10use language::Selection;
  11use serde::Deserialize;
  12use workspace::Workspace;
  13
  14use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim};
  15
  16#[derive(Copy, Clone, Debug, PartialEq)]
  17pub enum Object {
  18    Word { ignore_punctuation: bool },
  19    Sentence,
  20    Quotes,
  21    BackQuotes,
  22    DoubleQuotes,
  23    VerticalBars,
  24    Parentheses,
  25    SquareBrackets,
  26    CurlyBrackets,
  27    AngleBrackets,
  28}
  29
  30#[derive(Action, Clone, Deserialize, PartialEq)]
  31#[serde(rename_all = "camelCase")]
  32struct Word {
  33    #[serde(default)]
  34    ignore_punctuation: bool,
  35}
  36
  37actions!(
  38    Sentence,
  39    Quotes,
  40    BackQuotes,
  41    DoubleQuotes,
  42    VerticalBars,
  43    Parentheses,
  44    SquareBrackets,
  45    CurlyBrackets,
  46    AngleBrackets
  47);
  48
  49pub fn init(cx: &mut AppContext) {
  50    // todo!()
  51    // cx.add_action(
  52    //     |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
  53    //         object(Object::Word { ignore_punctuation }, cx)
  54    //     },
  55    // );
  56    // cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
  57    // cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
  58    // cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
  59    // cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
  60    // cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
  61    // cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
  62    //     object(Object::SquareBrackets, cx)
  63    // });
  64    // cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
  65    // cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
  66    // cx.add_action(|_: &mut Workspace, _: &VerticalBars, cx: _| object(Object::VerticalBars, cx));
  67}
  68
  69fn object(object: Object, cx: &mut WindowContext) {
  70    match Vim::read(cx).state().mode {
  71        Mode::Normal => normal_object(object, cx),
  72        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
  73        Mode::Insert => {
  74            // Shouldn't execute a text object in insert mode. Ignoring
  75        }
  76    }
  77}
  78
  79impl Object {
  80    pub fn is_multiline(self) -> bool {
  81        match self {
  82            Object::Word { .. }
  83            | Object::Quotes
  84            | Object::BackQuotes
  85            | Object::VerticalBars
  86            | Object::DoubleQuotes => false,
  87            Object::Sentence
  88            | Object::Parentheses
  89            | Object::AngleBrackets
  90            | Object::CurlyBrackets
  91            | Object::SquareBrackets => true,
  92        }
  93    }
  94
  95    pub fn always_expands_both_ways(self) -> bool {
  96        match self {
  97            Object::Word { .. } | Object::Sentence => false,
  98            Object::Quotes
  99            | Object::BackQuotes
 100            | Object::DoubleQuotes
 101            | Object::VerticalBars
 102            | Object::Parentheses
 103            | Object::SquareBrackets
 104            | Object::CurlyBrackets
 105            | Object::AngleBrackets => true,
 106        }
 107    }
 108
 109    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
 110        match self {
 111            Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual,
 112            Object::Word { .. } => current_mode,
 113            Object::Sentence
 114            | Object::Quotes
 115            | Object::BackQuotes
 116            | Object::DoubleQuotes
 117            | Object::VerticalBars
 118            | Object::Parentheses
 119            | Object::SquareBrackets
 120            | Object::CurlyBrackets
 121            | Object::AngleBrackets => Mode::Visual,
 122        }
 123    }
 124
 125    pub fn range(
 126        self,
 127        map: &DisplaySnapshot,
 128        relative_to: DisplayPoint,
 129        around: bool,
 130    ) -> Option<Range<DisplayPoint>> {
 131        match self {
 132            Object::Word { ignore_punctuation } => {
 133                if around {
 134                    around_word(map, relative_to, ignore_punctuation)
 135                } else {
 136                    in_word(map, relative_to, ignore_punctuation)
 137                }
 138            }
 139            Object::Sentence => sentence(map, relative_to, around),
 140            Object::Quotes => {
 141                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 142            }
 143            Object::BackQuotes => {
 144                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 145            }
 146            Object::DoubleQuotes => {
 147                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 148            }
 149            Object::VerticalBars => {
 150                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 151            }
 152            Object::Parentheses => {
 153                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 154            }
 155            Object::SquareBrackets => {
 156                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 157            }
 158            Object::CurlyBrackets => {
 159                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 160            }
 161            Object::AngleBrackets => {
 162                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 163            }
 164        }
 165    }
 166
 167    pub fn expand_selection(
 168        self,
 169        map: &DisplaySnapshot,
 170        selection: &mut Selection<DisplayPoint>,
 171        around: bool,
 172    ) -> bool {
 173        if let Some(range) = self.range(map, selection.head(), around) {
 174            selection.start = range.start;
 175            selection.end = range.end;
 176            true
 177        } else {
 178            false
 179        }
 180    }
 181}
 182
 183/// Return a range that surrounds the word relative_to is in
 184/// If relative_to is at the start of a word, return the word.
 185/// If relative_to is between words, return the space between
 186fn in_word(
 187    map: &DisplaySnapshot,
 188    relative_to: DisplayPoint,
 189    ignore_punctuation: bool,
 190) -> Option<Range<DisplayPoint>> {
 191    // Use motion::right so that we consider the character under the cursor when looking for the start
 192    let scope = map
 193        .buffer_snapshot
 194        .language_scope_at(relative_to.to_point(map));
 195    let start = movement::find_preceding_boundary(
 196        map,
 197        right(map, relative_to, 1),
 198        movement::FindRange::SingleLine,
 199        |left, right| {
 200            char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
 201                != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
 202        },
 203    );
 204
 205    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 206        char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
 207            != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
 208    });
 209
 210    Some(start..end)
 211}
 212
 213/// Return a range that surrounds the word and following whitespace
 214/// relative_to is in.
 215/// If relative_to is at the start of a word, return the word and following whitespace.
 216/// If relative_to is between words, return the whitespace back and the following word
 217
 218/// if in word
 219///   delete that word
 220///   if there is whitespace following the word, delete that as well
 221///   otherwise, delete any preceding whitespace
 222/// otherwise
 223///   delete whitespace around cursor
 224///   delete word following the cursor
 225fn around_word(
 226    map: &DisplaySnapshot,
 227    relative_to: DisplayPoint,
 228    ignore_punctuation: bool,
 229) -> Option<Range<DisplayPoint>> {
 230    let scope = map
 231        .buffer_snapshot
 232        .language_scope_at(relative_to.to_point(map));
 233    let in_word = map
 234        .chars_at(relative_to)
 235        .next()
 236        .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
 237        .unwrap_or(false);
 238
 239    if in_word {
 240        around_containing_word(map, relative_to, ignore_punctuation)
 241    } else {
 242        around_next_word(map, relative_to, ignore_punctuation)
 243    }
 244}
 245
 246fn around_containing_word(
 247    map: &DisplaySnapshot,
 248    relative_to: DisplayPoint,
 249    ignore_punctuation: bool,
 250) -> Option<Range<DisplayPoint>> {
 251    in_word(map, relative_to, ignore_punctuation)
 252        .map(|range| expand_to_include_whitespace(map, range, true))
 253}
 254
 255fn around_next_word(
 256    map: &DisplaySnapshot,
 257    relative_to: DisplayPoint,
 258    ignore_punctuation: bool,
 259) -> Option<Range<DisplayPoint>> {
 260    let scope = map
 261        .buffer_snapshot
 262        .language_scope_at(relative_to.to_point(map));
 263    // Get the start of the word
 264    let start = movement::find_preceding_boundary(
 265        map,
 266        right(map, relative_to, 1),
 267        FindRange::SingleLine,
 268        |left, right| {
 269            char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
 270                != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
 271        },
 272    );
 273
 274    let mut word_found = false;
 275    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 276        let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 277        let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 278
 279        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 280
 281        if right_kind != CharKind::Whitespace {
 282            word_found = true;
 283        }
 284
 285        found
 286    });
 287
 288    Some(start..end)
 289}
 290
 291fn sentence(
 292    map: &DisplaySnapshot,
 293    relative_to: DisplayPoint,
 294    around: bool,
 295) -> Option<Range<DisplayPoint>> {
 296    let mut start = None;
 297    let mut previous_end = relative_to;
 298
 299    let mut chars = map.chars_at(relative_to).peekable();
 300
 301    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 302    for (char, point) in chars
 303        .peek()
 304        .cloned()
 305        .into_iter()
 306        .chain(map.reverse_chars_at(relative_to))
 307    {
 308        if is_sentence_end(map, point) {
 309            break;
 310        }
 311
 312        if is_possible_sentence_start(char) {
 313            start = Some(point);
 314        }
 315
 316        previous_end = point;
 317    }
 318
 319    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 320    let mut end = relative_to;
 321    for (char, point) in chars {
 322        if start.is_none() && is_possible_sentence_start(char) {
 323            if around {
 324                start = Some(point);
 325                continue;
 326            } else {
 327                end = point;
 328                break;
 329            }
 330        }
 331
 332        end = point;
 333        *end.column_mut() += char.len_utf8() as u32;
 334        end = map.clip_point(end, Bias::Left);
 335
 336        if is_sentence_end(map, end) {
 337            break;
 338        }
 339    }
 340
 341    let mut range = start.unwrap_or(previous_end)..end;
 342    if around {
 343        range = expand_to_include_whitespace(map, range, false);
 344    }
 345
 346    Some(range)
 347}
 348
 349fn is_possible_sentence_start(character: char) -> bool {
 350    !character.is_whitespace() && character != '.'
 351}
 352
 353const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 354const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 355const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 356fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
 357    let mut next_chars = map.chars_at(point).peekable();
 358    if let Some((char, _)) = next_chars.next() {
 359        // We are at a double newline. This position is a sentence end.
 360        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 361            return true;
 362        }
 363
 364        // The next text is not a valid whitespace. This is not a sentence end
 365        if !SENTENCE_END_WHITESPACE.contains(&char) {
 366            return false;
 367        }
 368    }
 369
 370    for (char, _) in map.reverse_chars_at(point) {
 371        if SENTENCE_END_PUNCTUATION.contains(&char) {
 372            return true;
 373        }
 374
 375        if !SENTENCE_END_FILLERS.contains(&char) {
 376            return false;
 377        }
 378    }
 379
 380    return false;
 381}
 382
 383/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 384/// whitespace to the end first and falls back to the start if there was none.
 385fn expand_to_include_whitespace(
 386    map: &DisplaySnapshot,
 387    mut range: Range<DisplayPoint>,
 388    stop_at_newline: bool,
 389) -> Range<DisplayPoint> {
 390    let mut whitespace_included = false;
 391
 392    let mut chars = map.chars_at(range.end).peekable();
 393    while let Some((char, point)) = chars.next() {
 394        if char == '\n' && stop_at_newline {
 395            break;
 396        }
 397
 398        if char.is_whitespace() {
 399            // Set end to the next display_point or the character position after the current display_point
 400            range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
 401                let mut end = point;
 402                *end.column_mut() += char.len_utf8() as u32;
 403                map.clip_point(end, Bias::Left)
 404            });
 405
 406            if char != '\n' {
 407                whitespace_included = true;
 408            }
 409        } else {
 410            // Found non whitespace. Quit out.
 411            break;
 412        }
 413    }
 414
 415    if !whitespace_included {
 416        for (char, point) in map.reverse_chars_at(range.start) {
 417            if char == '\n' && stop_at_newline {
 418                break;
 419            }
 420
 421            if !char.is_whitespace() {
 422                break;
 423            }
 424
 425            range.start = point;
 426        }
 427    }
 428
 429    range
 430}
 431
 432fn surrounding_markers(
 433    map: &DisplaySnapshot,
 434    relative_to: DisplayPoint,
 435    around: bool,
 436    search_across_lines: bool,
 437    open_marker: char,
 438    close_marker: char,
 439) -> Option<Range<DisplayPoint>> {
 440    let point = relative_to.to_offset(map, Bias::Left);
 441
 442    let mut matched_closes = 0;
 443    let mut opening = None;
 444
 445    if let Some((ch, range)) = movement::chars_after(map, point).next() {
 446        if ch == open_marker {
 447            if open_marker == close_marker {
 448                let mut total = 0;
 449                for (ch, _) in movement::chars_before(map, point) {
 450                    if ch == '\n' {
 451                        break;
 452                    }
 453                    if ch == open_marker {
 454                        total += 1;
 455                    }
 456                }
 457                if total % 2 == 0 {
 458                    opening = Some(range)
 459                }
 460            } else {
 461                opening = Some(range)
 462            }
 463        }
 464    }
 465
 466    if opening.is_none() {
 467        for (ch, range) in movement::chars_before(map, point) {
 468            if ch == '\n' && !search_across_lines {
 469                break;
 470            }
 471
 472            if ch == open_marker {
 473                if matched_closes == 0 {
 474                    opening = Some(range);
 475                    break;
 476                }
 477                matched_closes -= 1;
 478            } else if ch == close_marker {
 479                matched_closes += 1
 480            }
 481        }
 482    }
 483
 484    if opening.is_none() {
 485        for (ch, range) in movement::chars_after(map, point) {
 486            if ch == open_marker {
 487                opening = Some(range);
 488                break;
 489            } else if ch == close_marker {
 490                break;
 491            }
 492        }
 493    }
 494
 495    let Some(mut opening) = opening else {
 496        return None;
 497    };
 498
 499    let mut matched_opens = 0;
 500    let mut closing = None;
 501
 502    for (ch, range) in movement::chars_after(map, opening.end) {
 503        if ch == '\n' && !search_across_lines {
 504            break;
 505        }
 506
 507        if ch == close_marker {
 508            if matched_opens == 0 {
 509                closing = Some(range);
 510                break;
 511            }
 512            matched_opens -= 1;
 513        } else if ch == open_marker {
 514            matched_opens += 1;
 515        }
 516    }
 517
 518    let Some(mut closing) = closing else {
 519        return None;
 520    };
 521
 522    if around && !search_across_lines {
 523        let mut found = false;
 524
 525        for (ch, range) in movement::chars_after(map, closing.end) {
 526            if ch.is_whitespace() && ch != '\n' {
 527                found = true;
 528                closing.end = range.end;
 529            } else {
 530                break;
 531            }
 532        }
 533
 534        if !found {
 535            for (ch, range) in movement::chars_before(map, opening.start) {
 536                if ch.is_whitespace() && ch != '\n' {
 537                    opening.start = range.start
 538                } else {
 539                    break;
 540                }
 541            }
 542        }
 543    }
 544
 545    if !around && search_across_lines {
 546        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
 547            if ch == '\n' {
 548                opening.end = range.end
 549            }
 550        }
 551
 552        for (ch, range) in movement::chars_before(map, closing.start) {
 553            if !ch.is_whitespace() {
 554                break;
 555            }
 556            if ch != '\n' {
 557                closing.start = range.start
 558            }
 559        }
 560    }
 561
 562    let result = if around {
 563        opening.start..closing.end
 564    } else {
 565        opening.end..closing.start
 566    };
 567
 568    Some(
 569        map.clip_point(result.start.to_display_point(map), Bias::Left)
 570            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
 571    )
 572}
 573
 574// #[cfg(test)]
 575// mod test {
 576//     use indoc::indoc;
 577
 578//     use crate::{
 579//         state::Mode,
 580//         test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
 581//     };
 582
 583//     const WORD_LOCATIONS: &'static str = indoc! {"
 584//         The quick ˇbrowˇnˇ•••
 585//         fox ˇjuˇmpsˇ over
 586//         the lazy dogˇ••
 587//         ˇ
 588//         ˇ
 589//         ˇ
 590//         Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
 591//         ˇ••
 592//         ˇ••
 593//         ˇ  fox-jumpˇs over
 594//         the lazy dogˇ•
 595//         ˇ
 596//         "
 597//     };
 598
 599//     #[gpui::test]
 600//     async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
 601//         let mut cx = NeovimBackedTestContext::new(cx).await;
 602
 603//         cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
 604//             .await;
 605//         cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
 606//             .await;
 607//         cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
 608//             .await;
 609//         cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
 610//             .await;
 611//     }
 612
 613//     #[gpui::test]
 614//     async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
 615//         let mut cx = NeovimBackedTestContext::new(cx).await;
 616
 617//         cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
 618//             .await;
 619//         cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
 620//             .await;
 621//         cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
 622//             .await;
 623//         cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
 624//             .await;
 625//     }
 626
 627//     #[gpui::test]
 628//     async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
 629//         let mut cx = NeovimBackedTestContext::new(cx).await;
 630
 631//         /*
 632//                 cx.set_shared_state("The quick ˇbrown\nfox").await;
 633//                 cx.simulate_shared_keystrokes(["v"]).await;
 634//                 cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
 635//                 cx.simulate_shared_keystrokes(["i", "w"]).await;
 636//                 cx.assert_shared_state("The quick «brownˇ»\nfox").await;
 637//         */
 638//         cx.set_shared_state("The quick brown\nˇ\nfox").await;
 639//         cx.simulate_shared_keystrokes(["v"]).await;
 640//         cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 641//         cx.simulate_shared_keystrokes(["i", "w"]).await;
 642//         cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 643
 644//         cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
 645//             .await;
 646//         cx.assert_binding_matches_all_exempted(
 647//             ["v", "h", "i", "w"],
 648//             WORD_LOCATIONS,
 649//             ExemptionFeatures::NonEmptyVisualTextObjects,
 650//         )
 651//         .await;
 652//         cx.assert_binding_matches_all_exempted(
 653//             ["v", "l", "i", "w"],
 654//             WORD_LOCATIONS,
 655//             ExemptionFeatures::NonEmptyVisualTextObjects,
 656//         )
 657//         .await;
 658//         cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
 659//             .await;
 660
 661//         cx.assert_binding_matches_all_exempted(
 662//             ["v", "i", "h", "shift-w"],
 663//             WORD_LOCATIONS,
 664//             ExemptionFeatures::NonEmptyVisualTextObjects,
 665//         )
 666//         .await;
 667//         cx.assert_binding_matches_all_exempted(
 668//             ["v", "i", "l", "shift-w"],
 669//             WORD_LOCATIONS,
 670//             ExemptionFeatures::NonEmptyVisualTextObjects,
 671//         )
 672//         .await;
 673
 674//         cx.assert_binding_matches_all_exempted(
 675//             ["v", "a", "w"],
 676//             WORD_LOCATIONS,
 677//             ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 678//         )
 679//         .await;
 680//         cx.assert_binding_matches_all_exempted(
 681//             ["v", "a", "shift-w"],
 682//             WORD_LOCATIONS,
 683//             ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 684//         )
 685//         .await;
 686//     }
 687
 688//     const SENTENCE_EXAMPLES: &[&'static str] = &[
 689//         "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
 690//         indoc! {"
 691//             ˇThe quick ˇbrownˇ
 692//             fox jumps over
 693//             the lazy doˇgˇ.ˇ ˇThe quick ˇ
 694//             brown fox jumps over
 695//         "},
 696//         indoc! {"
 697//             The quick brown fox jumps.
 698//             Over the lazy dog
 699//             ˇ
 700//             ˇ
 701//             ˇ  fox-jumpˇs over
 702//             the lazy dog.ˇ
 703//             ˇ
 704//         "},
 705//         r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
 706//     ];
 707
 708//     #[gpui::test]
 709//     async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
 710//         let mut cx = NeovimBackedTestContext::new(cx)
 711//             .await
 712//             .binding(["c", "i", "s"]);
 713//         cx.add_initial_state_exemptions(
 714//             "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
 715//             ExemptionFeatures::SentenceOnEmptyLines);
 716//         cx.add_initial_state_exemptions(
 717//             "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
 718//             ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
 719//         cx.add_initial_state_exemptions(
 720//             "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
 721//             ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
 722//         for sentence_example in SENTENCE_EXAMPLES {
 723//             cx.assert_all(sentence_example).await;
 724//         }
 725
 726//         let mut cx = cx.binding(["c", "a", "s"]);
 727//         cx.add_initial_state_exemptions(
 728//             "The quick brown?ˇ Fox Jumps! Over the lazy.",
 729//             ExemptionFeatures::IncorrectLandingPosition,
 730//         );
 731//         cx.add_initial_state_exemptions(
 732//             "The quick brown.)]\'\" Brown fox jumps.ˇ ",
 733//             ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 734//         );
 735
 736//         for sentence_example in SENTENCE_EXAMPLES {
 737//             cx.assert_all(sentence_example).await;
 738//         }
 739//     }
 740
 741//     #[gpui::test]
 742//     async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
 743//         let mut cx = NeovimBackedTestContext::new(cx)
 744//             .await
 745//             .binding(["d", "i", "s"]);
 746//         cx.add_initial_state_exemptions(
 747//             "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
 748//             ExemptionFeatures::SentenceOnEmptyLines);
 749//         cx.add_initial_state_exemptions(
 750//             "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
 751//             ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
 752//         cx.add_initial_state_exemptions(
 753//             "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
 754//             ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
 755
 756//         for sentence_example in SENTENCE_EXAMPLES {
 757//             cx.assert_all(sentence_example).await;
 758//         }
 759
 760//         let mut cx = cx.binding(["d", "a", "s"]);
 761//         cx.add_initial_state_exemptions(
 762//             "The quick brown?ˇ Fox Jumps! Over the lazy.",
 763//             ExemptionFeatures::IncorrectLandingPosition,
 764//         );
 765//         cx.add_initial_state_exemptions(
 766//             "The quick brown.)]\'\" Brown fox jumps.ˇ ",
 767//             ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 768//         );
 769
 770//         for sentence_example in SENTENCE_EXAMPLES {
 771//             cx.assert_all(sentence_example).await;
 772//         }
 773//     }
 774
 775//     #[gpui::test]
 776//     async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
 777//         let mut cx = NeovimBackedTestContext::new(cx)
 778//             .await
 779//             .binding(["v", "i", "s"]);
 780//         for sentence_example in SENTENCE_EXAMPLES {
 781//             cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
 782//                 .await;
 783//         }
 784
 785//         let mut cx = cx.binding(["v", "a", "s"]);
 786//         for sentence_example in SENTENCE_EXAMPLES {
 787//             cx.assert_all_exempted(
 788//                 sentence_example,
 789//                 ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
 790//             )
 791//             .await;
 792//         }
 793//     }
 794
 795//     // Test string with "`" for opening surrounders and "'" for closing surrounders
 796//     const SURROUNDING_MARKER_STRING: &str = indoc! {"
 797//         ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
 798//         'ˇfox juˇmps ovˇ`ˇer
 799//         the ˇlazy dˇ'ˇoˇ`ˇg"};
 800
 801//     const SURROUNDING_OBJECTS: &[(char, char)] = &[
 802//         ('\'', '\''), // Quote
 803//         ('`', '`'),   // Back Quote
 804//         ('"', '"'),   // Double Quote
 805//         ('(', ')'),   // Parentheses
 806//         ('[', ']'),   // SquareBrackets
 807//         ('{', '}'),   // CurlyBrackets
 808//         ('<', '>'),   // AngleBrackets
 809//     ];
 810
 811//     #[gpui::test]
 812//     async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
 813//         let mut cx = NeovimBackedTestContext::new(cx).await;
 814
 815//         for (start, end) in SURROUNDING_OBJECTS {
 816//             let marked_string = SURROUNDING_MARKER_STRING
 817//                 .replace('`', &start.to_string())
 818//                 .replace('\'', &end.to_string());
 819
 820//             cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
 821//                 .await;
 822//             cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
 823//                 .await;
 824//             cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
 825//                 .await;
 826//             cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
 827//                 .await;
 828//         }
 829//     }
 830//     #[gpui::test]
 831//     async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
 832//         let mut cx = NeovimBackedTestContext::new(cx).await;
 833//         cx.set_shared_wrap(12).await;
 834
 835//         cx.set_shared_state(indoc! {
 836//             "helˇlo \"world\"!"
 837//         })
 838//         .await;
 839//         cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
 840//         cx.assert_shared_state(indoc! {
 841//             "hello \"«worldˇ»\"!"
 842//         })
 843//         .await;
 844
 845//         cx.set_shared_state(indoc! {
 846//             "hello \"wˇorld\"!"
 847//         })
 848//         .await;
 849//         cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
 850//         cx.assert_shared_state(indoc! {
 851//             "hello \"«worldˇ»\"!"
 852//         })
 853//         .await;
 854
 855//         cx.set_shared_state(indoc! {
 856//             "hello \"wˇorld\"!"
 857//         })
 858//         .await;
 859//         cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
 860//         cx.assert_shared_state(indoc! {
 861//             "hello« \"world\"ˇ»!"
 862//         })
 863//         .await;
 864
 865//         cx.set_shared_state(indoc! {
 866//             "hello \"wˇorld\" !"
 867//         })
 868//         .await;
 869//         cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
 870//         cx.assert_shared_state(indoc! {
 871//             "hello «\"world\" ˇ»!"
 872//         })
 873//         .await;
 874
 875//         cx.set_shared_state(indoc! {
 876//             "hello \"wˇorld\"•
 877//             goodbye"
 878//         })
 879//         .await;
 880//         cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
 881//         cx.assert_shared_state(indoc! {
 882//             "hello «\"world\" ˇ»
 883//             goodbye"
 884//         })
 885//         .await;
 886//     }
 887
 888//     #[gpui::test]
 889//     async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
 890//         let mut cx = NeovimBackedTestContext::new(cx).await;
 891
 892//         cx.set_shared_state(indoc! {
 893//             "func empty(a string) bool {
 894//                if a == \"\" {
 895//                   return true
 896//                }
 897//                ˇreturn false
 898//             }"
 899//         })
 900//         .await;
 901//         cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
 902//         cx.assert_shared_state(indoc! {"
 903//             func empty(a string) bool {
 904//             «   if a == \"\" {
 905//                   return true
 906//                }
 907//                return false
 908//             ˇ»}"})
 909//             .await;
 910//         cx.set_shared_state(indoc! {
 911//             "func empty(a string) bool {
 912//                  if a == \"\" {
 913//                      ˇreturn true
 914//                  }
 915//                  return false
 916//             }"
 917//         })
 918//         .await;
 919//         cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
 920//         cx.assert_shared_state(indoc! {"
 921//             func empty(a string) bool {
 922//                  if a == \"\" {
 923//             «         return true
 924//             ˇ»     }
 925//                  return false
 926//             }"})
 927//             .await;
 928
 929//         cx.set_shared_state(indoc! {
 930//             "func empty(a string) bool {
 931//                  if a == \"\" ˇ{
 932//                      return true
 933//                  }
 934//                  return false
 935//             }"
 936//         })
 937//         .await;
 938//         cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
 939//         cx.assert_shared_state(indoc! {"
 940//             func empty(a string) bool {
 941//                  if a == \"\" {
 942//             «         return true
 943//             ˇ»     }
 944//                  return false
 945//             }"})
 946//             .await;
 947//     }
 948
 949//     #[gpui::test]
 950//     async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
 951//         let mut cx = VimTestContext::new(cx, true).await;
 952//         cx.set_state(
 953//             indoc! {"
 954//             fn boop() {
 955//                 baz(ˇ|a, b| { bar(|j, k| { })})
 956//             }"
 957//             },
 958//             Mode::Normal,
 959//         );
 960//         cx.simulate_keystrokes(["c", "i", "|"]);
 961//         cx.assert_state(
 962//             indoc! {"
 963//             fn boop() {
 964//                 baz(|ˇ| { bar(|j, k| { })})
 965//             }"
 966//             },
 967//             Mode::Insert,
 968//         );
 969//         cx.simulate_keystrokes(["escape", "1", "8", "|"]);
 970//         cx.assert_state(
 971//             indoc! {"
 972//             fn boop() {
 973//                 baz(|| { bar(ˇ|j, k| { })})
 974//             }"
 975//             },
 976//             Mode::Normal,
 977//         );
 978
 979//         cx.simulate_keystrokes(["v", "a", "|"]);
 980//         cx.assert_state(
 981//             indoc! {"
 982//             fn boop() {
 983//                 baz(|| { bar(«|j, k| ˇ»{ })})
 984//             }"
 985//             },
 986//             Mode::Visual,
 987//         );
 988//     }
 989
 990//     #[gpui::test]
 991//     async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
 992//         let mut cx = NeovimBackedTestContext::new(cx).await;
 993
 994//         for (start, end) in SURROUNDING_OBJECTS {
 995//             let marked_string = SURROUNDING_MARKER_STRING
 996//                 .replace('`', &start.to_string())
 997//                 .replace('\'', &end.to_string());
 998
 999//             cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
1000//                 .await;
1001//             cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
1002//                 .await;
1003//             cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
1004//                 .await;
1005//             cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
1006//                 .await;
1007//         }
1008//     }
1009// }