object.rs

   1use std::ops::Range;
   2
   3use editor::{
   4    display_map::{DisplaySnapshot, ToDisplayPoint},
   5    movement::{self, FindRange},
   6    Bias, DisplayPoint,
   7};
   8use gpui::{actions, impl_actions, ViewContext, WindowContext};
   9use language::{char_kind, BufferSnapshot, CharKind, Selection};
  10use serde::Deserialize;
  11use workspace::Workspace;
  12
  13use crate::{
  14    motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation,
  15    visual::visual_object, Vim,
  16};
  17
  18#[derive(Copy, Clone, Debug, PartialEq)]
  19pub enum Object {
  20    Word { ignore_punctuation: bool },
  21    Sentence,
  22    Quotes,
  23    BackQuotes,
  24    DoubleQuotes,
  25    VerticalBars,
  26    Parentheses,
  27    SquareBrackets,
  28    CurlyBrackets,
  29    AngleBrackets,
  30    Argument,
  31}
  32
  33#[derive(Clone, Deserialize, PartialEq)]
  34#[serde(rename_all = "camelCase")]
  35struct Word {
  36    #[serde(default)]
  37    ignore_punctuation: bool,
  38}
  39
  40impl_actions!(vim, [Word]);
  41
  42actions!(
  43    vim,
  44    [
  45        Sentence,
  46        Quotes,
  47        BackQuotes,
  48        DoubleQuotes,
  49        VerticalBars,
  50        Parentheses,
  51        SquareBrackets,
  52        CurlyBrackets,
  53        AngleBrackets,
  54        Argument
  55    ]
  56);
  57
  58pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  59    workspace.register_action(
  60        |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
  61            object(Object::Word { ignore_punctuation }, cx)
  62        },
  63    );
  64    workspace
  65        .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
  66    workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
  67    workspace
  68        .register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
  69    workspace.register_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| {
  70        object(Object::DoubleQuotes, cx)
  71    });
  72    workspace.register_action(|_: &mut Workspace, _: &Parentheses, cx: _| {
  73        object(Object::Parentheses, cx)
  74    });
  75    workspace.register_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
  76        object(Object::SquareBrackets, cx)
  77    });
  78    workspace.register_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| {
  79        object(Object::CurlyBrackets, cx)
  80    });
  81    workspace.register_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| {
  82        object(Object::AngleBrackets, cx)
  83    });
  84    workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
  85        object(Object::VerticalBars, cx)
  86    });
  87    workspace
  88        .register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx));
  89}
  90
  91fn object(object: Object, cx: &mut WindowContext) {
  92    match Vim::read(cx).state().mode {
  93        Mode::Normal => normal_object(object, cx),
  94        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
  95        Mode::Insert => {
  96            // Shouldn't execute a text object in insert mode. Ignoring
  97        }
  98    }
  99}
 100
 101impl Object {
 102    pub fn is_multiline(self) -> bool {
 103        match self {
 104            Object::Word { .. }
 105            | Object::Quotes
 106            | Object::BackQuotes
 107            | Object::VerticalBars
 108            | Object::DoubleQuotes => false,
 109            Object::Sentence
 110            | Object::Parentheses
 111            | Object::AngleBrackets
 112            | Object::CurlyBrackets
 113            | Object::SquareBrackets
 114            | Object::Argument => true,
 115        }
 116    }
 117
 118    pub fn always_expands_both_ways(self) -> bool {
 119        match self {
 120            Object::Word { .. } | Object::Sentence | Object::Argument => false,
 121            Object::Quotes
 122            | Object::BackQuotes
 123            | Object::DoubleQuotes
 124            | Object::VerticalBars
 125            | Object::Parentheses
 126            | Object::SquareBrackets
 127            | Object::CurlyBrackets
 128            | Object::AngleBrackets => true,
 129        }
 130    }
 131
 132    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
 133        match self {
 134            Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual,
 135            Object::Word { .. } => current_mode,
 136            Object::Sentence
 137            | Object::Quotes
 138            | Object::BackQuotes
 139            | Object::DoubleQuotes
 140            | Object::VerticalBars
 141            | Object::Parentheses
 142            | Object::SquareBrackets
 143            | Object::CurlyBrackets
 144            | Object::AngleBrackets
 145            | Object::Argument => Mode::Visual,
 146        }
 147    }
 148
 149    pub fn range(
 150        self,
 151        map: &DisplaySnapshot,
 152        relative_to: DisplayPoint,
 153        around: bool,
 154    ) -> Option<Range<DisplayPoint>> {
 155        match self {
 156            Object::Word { ignore_punctuation } => {
 157                if around {
 158                    around_word(map, relative_to, ignore_punctuation)
 159                } else {
 160                    in_word(map, relative_to, ignore_punctuation)
 161                }
 162            }
 163            Object::Sentence => sentence(map, relative_to, around),
 164            Object::Quotes => {
 165                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 166            }
 167            Object::BackQuotes => {
 168                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 169            }
 170            Object::DoubleQuotes => {
 171                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 172            }
 173            Object::VerticalBars => {
 174                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 175            }
 176            Object::Parentheses => {
 177                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 178            }
 179            Object::SquareBrackets => {
 180                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 181            }
 182            Object::CurlyBrackets => {
 183                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 184            }
 185            Object::AngleBrackets => {
 186                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 187            }
 188            Object::Argument => argument(map, relative_to, around),
 189        }
 190    }
 191
 192    pub fn expand_selection(
 193        self,
 194        map: &DisplaySnapshot,
 195        selection: &mut Selection<DisplayPoint>,
 196        around: bool,
 197    ) -> bool {
 198        if let Some(range) = self.range(map, selection.head(), around) {
 199            selection.start = range.start;
 200            selection.end = range.end;
 201            true
 202        } else {
 203            false
 204        }
 205    }
 206}
 207
 208/// Returns a range that surrounds the word `relative_to` is in.
 209///
 210/// If `relative_to` is at the start of a word, return the word.
 211/// If `relative_to` is between words, return the space between.
 212fn in_word(
 213    map: &DisplaySnapshot,
 214    relative_to: DisplayPoint,
 215    ignore_punctuation: bool,
 216) -> Option<Range<DisplayPoint>> {
 217    // Use motion::right so that we consider the character under the cursor when looking for the start
 218    let scope = map
 219        .buffer_snapshot
 220        .language_scope_at(relative_to.to_point(map));
 221    let start = movement::find_preceding_boundary_display_point(
 222        map,
 223        right(map, relative_to, 1),
 224        movement::FindRange::SingleLine,
 225        |left, right| {
 226            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 227                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 228        },
 229    );
 230
 231    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 232        coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 233            != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 234    });
 235
 236    Some(start..end)
 237}
 238
 239/// Returns a range that surrounds the word and following whitespace
 240/// relative_to is in.
 241///
 242/// If `relative_to` is at the start of a word, return the word and following whitespace.
 243/// If `relative_to` is between words, return the whitespace back and the following word.
 244///
 245/// if in word
 246///   delete that word
 247///   if there is whitespace following the word, delete that as well
 248///   otherwise, delete any preceding whitespace
 249/// otherwise
 250///   delete whitespace around cursor
 251///   delete word following the cursor
 252fn around_word(
 253    map: &DisplaySnapshot,
 254    relative_to: DisplayPoint,
 255    ignore_punctuation: bool,
 256) -> Option<Range<DisplayPoint>> {
 257    let scope = map
 258        .buffer_snapshot
 259        .language_scope_at(relative_to.to_point(map));
 260    let in_word = map
 261        .chars_at(relative_to)
 262        .next()
 263        .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
 264        .unwrap_or(false);
 265
 266    if in_word {
 267        around_containing_word(map, relative_to, ignore_punctuation)
 268    } else {
 269        around_next_word(map, relative_to, ignore_punctuation)
 270    }
 271}
 272
 273fn around_containing_word(
 274    map: &DisplaySnapshot,
 275    relative_to: DisplayPoint,
 276    ignore_punctuation: bool,
 277) -> Option<Range<DisplayPoint>> {
 278    in_word(map, relative_to, ignore_punctuation)
 279        .map(|range| expand_to_include_whitespace(map, range, true))
 280}
 281
 282fn around_next_word(
 283    map: &DisplaySnapshot,
 284    relative_to: DisplayPoint,
 285    ignore_punctuation: bool,
 286) -> Option<Range<DisplayPoint>> {
 287    let scope = map
 288        .buffer_snapshot
 289        .language_scope_at(relative_to.to_point(map));
 290    // Get the start of the word
 291    let start = movement::find_preceding_boundary_display_point(
 292        map,
 293        right(map, relative_to, 1),
 294        FindRange::SingleLine,
 295        |left, right| {
 296            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 297                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 298        },
 299    );
 300
 301    let mut word_found = false;
 302    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 303        let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 304        let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 305
 306        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 307
 308        if right_kind != CharKind::Whitespace {
 309            word_found = true;
 310        }
 311
 312        found
 313    });
 314
 315    Some(start..end)
 316}
 317
 318fn argument(
 319    map: &DisplaySnapshot,
 320    relative_to: DisplayPoint,
 321    around: bool,
 322) -> Option<Range<DisplayPoint>> {
 323    let snapshot = &map.buffer_snapshot;
 324    let offset = relative_to.to_offset(map, Bias::Left);
 325
 326    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 327    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 328    let buffer = excerpt.buffer();
 329
 330    fn comma_delimited_range_at(
 331        buffer: &BufferSnapshot,
 332        mut offset: usize,
 333        include_comma: bool,
 334    ) -> Option<Range<usize>> {
 335        // Seek to the first non-whitespace character
 336        offset += buffer
 337            .chars_at(offset)
 338            .take_while(|c| c.is_whitespace())
 339            .map(char::len_utf8)
 340            .sum::<usize>();
 341
 342        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 343            // Filter out empty ranges
 344            if open.end == close.start {
 345                return false;
 346            }
 347
 348            // If the cursor is outside the brackets, ignore them
 349            if open.start == offset || close.end == offset {
 350                return false;
 351            }
 352
 353            // TODO: Is there any better way to filter out string brackets?
 354            // Used to filter out string brackets
 355            return matches!(
 356                buffer.chars_at(open.start).next(),
 357                Some('(' | '[' | '{' | '<' | '|')
 358            );
 359        };
 360
 361        // Find the brackets containing the cursor
 362        let (open_bracket, close_bracket) =
 363            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 364
 365        let inner_bracket_range = open_bracket.end..close_bracket.start;
 366
 367        let layer = buffer.syntax_layer_at(offset)?;
 368        let node = layer.node();
 369        let mut cursor = node.walk();
 370
 371        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 372        let mut parent_covers_bracket_range = false;
 373        loop {
 374            let node = cursor.node();
 375            let range = node.byte_range();
 376            let covers_bracket_range =
 377                range.start == open_bracket.start && range.end == close_bracket.end;
 378            if parent_covers_bracket_range && !covers_bracket_range {
 379                break;
 380            }
 381            parent_covers_bracket_range = covers_bracket_range;
 382
 383            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 384            if !cursor.goto_first_child_for_byte(offset).is_some() {
 385                return None;
 386            }
 387        }
 388
 389        let mut argument_node = cursor.node();
 390
 391        // If the child node is the open bracket, move to the next sibling.
 392        if argument_node.byte_range() == open_bracket {
 393            if !cursor.goto_next_sibling() {
 394                return Some(inner_bracket_range);
 395            }
 396            argument_node = cursor.node();
 397        }
 398        // While the child node is the close bracket or a comma, move to the previous sibling
 399        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 400            if !cursor.goto_previous_sibling() {
 401                return Some(inner_bracket_range);
 402            }
 403            argument_node = cursor.node();
 404            if argument_node.byte_range() == open_bracket {
 405                return Some(inner_bracket_range);
 406            }
 407        }
 408
 409        // The start and end of the argument range, defaulting to the start and end of the argument node
 410        let mut start = argument_node.start_byte();
 411        let mut end = argument_node.end_byte();
 412
 413        let mut needs_surrounding_comma = include_comma;
 414
 415        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 416        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 417        while cursor.goto_previous_sibling() {
 418            let prev = cursor.node();
 419
 420            if prev.start_byte() < open_bracket.end {
 421                start = open_bracket.end;
 422                break;
 423            } else if prev.kind() == "," {
 424                if needs_surrounding_comma {
 425                    start = prev.start_byte();
 426                    needs_surrounding_comma = false;
 427                }
 428                break;
 429            } else if prev.start_byte() < start {
 430                start = prev.start_byte();
 431            }
 432        }
 433
 434        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 435        while cursor.goto_next_sibling() {
 436            let next = cursor.node();
 437
 438            if next.end_byte() > close_bracket.start {
 439                end = close_bracket.start;
 440                break;
 441            } else if next.kind() == "," {
 442                if needs_surrounding_comma {
 443                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 444                    if let Some(next_arg) = next.next_sibling() {
 445                        end = next_arg.start_byte();
 446                    } else {
 447                        end = next.end_byte();
 448                    }
 449                }
 450                break;
 451            } else if next.end_byte() > end {
 452                end = next.end_byte();
 453            }
 454        }
 455
 456        Some(start..end)
 457    }
 458
 459    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 460
 461    if excerpt.contains_buffer_range(result.clone()) {
 462        let result = excerpt.map_range_from_buffer(result);
 463        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 464    } else {
 465        None
 466    }
 467}
 468
 469fn sentence(
 470    map: &DisplaySnapshot,
 471    relative_to: DisplayPoint,
 472    around: bool,
 473) -> Option<Range<DisplayPoint>> {
 474    let mut start = None;
 475    let mut previous_end = relative_to;
 476
 477    let mut chars = map.chars_at(relative_to).peekable();
 478
 479    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 480    for (char, point) in chars
 481        .peek()
 482        .cloned()
 483        .into_iter()
 484        .chain(map.reverse_chars_at(relative_to))
 485    {
 486        if is_sentence_end(map, point) {
 487            break;
 488        }
 489
 490        if is_possible_sentence_start(char) {
 491            start = Some(point);
 492        }
 493
 494        previous_end = point;
 495    }
 496
 497    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 498    let mut end = relative_to;
 499    for (char, point) in chars {
 500        if start.is_none() && is_possible_sentence_start(char) {
 501            if around {
 502                start = Some(point);
 503                continue;
 504            } else {
 505                end = point;
 506                break;
 507            }
 508        }
 509
 510        end = point;
 511        *end.column_mut() += char.len_utf8() as u32;
 512        end = map.clip_point(end, Bias::Left);
 513
 514        if is_sentence_end(map, end) {
 515            break;
 516        }
 517    }
 518
 519    let mut range = start.unwrap_or(previous_end)..end;
 520    if around {
 521        range = expand_to_include_whitespace(map, range, false);
 522    }
 523
 524    Some(range)
 525}
 526
 527fn is_possible_sentence_start(character: char) -> bool {
 528    !character.is_whitespace() && character != '.'
 529}
 530
 531const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 532const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 533const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 534fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
 535    let mut next_chars = map.chars_at(point).peekable();
 536    if let Some((char, _)) = next_chars.next() {
 537        // We are at a double newline. This position is a sentence end.
 538        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 539            return true;
 540        }
 541
 542        // The next text is not a valid whitespace. This is not a sentence end
 543        if !SENTENCE_END_WHITESPACE.contains(&char) {
 544            return false;
 545        }
 546    }
 547
 548    for (char, _) in map.reverse_chars_at(point) {
 549        if SENTENCE_END_PUNCTUATION.contains(&char) {
 550            return true;
 551        }
 552
 553        if !SENTENCE_END_FILLERS.contains(&char) {
 554            return false;
 555        }
 556    }
 557
 558    return false;
 559}
 560
 561/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 562/// whitespace to the end first and falls back to the start if there was none.
 563fn expand_to_include_whitespace(
 564    map: &DisplaySnapshot,
 565    mut range: Range<DisplayPoint>,
 566    stop_at_newline: bool,
 567) -> Range<DisplayPoint> {
 568    let mut whitespace_included = false;
 569
 570    let mut chars = map.chars_at(range.end).peekable();
 571    while let Some((char, point)) = chars.next() {
 572        if char == '\n' && stop_at_newline {
 573            break;
 574        }
 575
 576        if char.is_whitespace() {
 577            // Set end to the next display_point or the character position after the current display_point
 578            range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
 579                let mut end = point;
 580                *end.column_mut() += char.len_utf8() as u32;
 581                map.clip_point(end, Bias::Left)
 582            });
 583
 584            if char != '\n' {
 585                whitespace_included = true;
 586            }
 587        } else {
 588            // Found non whitespace. Quit out.
 589            break;
 590        }
 591    }
 592
 593    if !whitespace_included {
 594        for (char, point) in map.reverse_chars_at(range.start) {
 595            if char == '\n' && stop_at_newline {
 596                break;
 597            }
 598
 599            if !char.is_whitespace() {
 600                break;
 601            }
 602
 603            range.start = point;
 604        }
 605    }
 606
 607    range
 608}
 609
 610fn surrounding_markers(
 611    map: &DisplaySnapshot,
 612    relative_to: DisplayPoint,
 613    around: bool,
 614    search_across_lines: bool,
 615    open_marker: char,
 616    close_marker: char,
 617) -> Option<Range<DisplayPoint>> {
 618    let point = relative_to.to_offset(map, Bias::Left);
 619
 620    let mut matched_closes = 0;
 621    let mut opening = None;
 622
 623    if let Some((ch, range)) = movement::chars_after(map, point).next() {
 624        if ch == open_marker {
 625            if open_marker == close_marker {
 626                let mut total = 0;
 627                for (ch, _) in movement::chars_before(map, point) {
 628                    if ch == '\n' {
 629                        break;
 630                    }
 631                    if ch == open_marker {
 632                        total += 1;
 633                    }
 634                }
 635                if total % 2 == 0 {
 636                    opening = Some(range)
 637                }
 638            } else {
 639                opening = Some(range)
 640            }
 641        }
 642    }
 643
 644    if opening.is_none() {
 645        for (ch, range) in movement::chars_before(map, point) {
 646            if ch == '\n' && !search_across_lines {
 647                break;
 648            }
 649
 650            if ch == open_marker {
 651                if matched_closes == 0 {
 652                    opening = Some(range);
 653                    break;
 654                }
 655                matched_closes -= 1;
 656            } else if ch == close_marker {
 657                matched_closes += 1
 658            }
 659        }
 660    }
 661
 662    if opening.is_none() {
 663        for (ch, range) in movement::chars_after(map, point) {
 664            if ch == open_marker {
 665                opening = Some(range);
 666                break;
 667            } else if ch == close_marker {
 668                break;
 669            }
 670        }
 671    }
 672
 673    let Some(mut opening) = opening else {
 674        return None;
 675    };
 676
 677    let mut matched_opens = 0;
 678    let mut closing = None;
 679
 680    for (ch, range) in movement::chars_after(map, opening.end) {
 681        if ch == '\n' && !search_across_lines {
 682            break;
 683        }
 684
 685        if ch == close_marker {
 686            if matched_opens == 0 {
 687                closing = Some(range);
 688                break;
 689            }
 690            matched_opens -= 1;
 691        } else if ch == open_marker {
 692            matched_opens += 1;
 693        }
 694    }
 695
 696    let Some(mut closing) = closing else {
 697        return None;
 698    };
 699
 700    if around && !search_across_lines {
 701        let mut found = false;
 702
 703        for (ch, range) in movement::chars_after(map, closing.end) {
 704            if ch.is_whitespace() && ch != '\n' {
 705                found = true;
 706                closing.end = range.end;
 707            } else {
 708                break;
 709            }
 710        }
 711
 712        if !found {
 713            for (ch, range) in movement::chars_before(map, opening.start) {
 714                if ch.is_whitespace() && ch != '\n' {
 715                    opening.start = range.start
 716                } else {
 717                    break;
 718                }
 719            }
 720        }
 721    }
 722
 723    if !around && search_across_lines {
 724        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
 725            if ch == '\n' {
 726                opening.end = range.end
 727            }
 728        }
 729
 730        for (ch, range) in movement::chars_before(map, closing.start) {
 731            if !ch.is_whitespace() {
 732                break;
 733            }
 734            if ch != '\n' {
 735                closing.start = range.start
 736            }
 737        }
 738    }
 739
 740    let result = if around {
 741        opening.start..closing.end
 742    } else {
 743        opening.end..closing.start
 744    };
 745
 746    Some(
 747        map.clip_point(result.start.to_display_point(map), Bias::Left)
 748            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
 749    )
 750}
 751
 752#[cfg(test)]
 753mod test {
 754    use indoc::indoc;
 755
 756    use crate::{
 757        state::Mode,
 758        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
 759    };
 760
 761    const WORD_LOCATIONS: &'static str = indoc! {"
 762        The quick ˇbrowˇnˇ•••
 763        fox ˇjuˇmpsˇ over
 764        the lazy dogˇ••
 765        ˇ
 766        ˇ
 767        ˇ
 768        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
 769        ˇ••
 770        ˇ••
 771        ˇ  fox-jumpˇs over
 772        the lazy dogˇ•
 773        ˇ
 774        "
 775    };
 776
 777    #[gpui::test]
 778    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
 779        let mut cx = NeovimBackedTestContext::new(cx).await;
 780
 781        cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
 782            .await;
 783        cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
 784            .await;
 785        cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
 786            .await;
 787        cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
 788            .await;
 789    }
 790
 791    #[gpui::test]
 792    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
 793        let mut cx = NeovimBackedTestContext::new(cx).await;
 794
 795        cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
 796            .await;
 797        cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
 798            .await;
 799        cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
 800            .await;
 801        cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
 802            .await;
 803    }
 804
 805    #[gpui::test]
 806    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
 807        let mut cx = NeovimBackedTestContext::new(cx).await;
 808
 809        /*
 810                cx.set_shared_state("The quick ˇbrown\nfox").await;
 811                cx.simulate_shared_keystrokes(["v"]).await;
 812                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
 813                cx.simulate_shared_keystrokes(["i", "w"]).await;
 814                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
 815        */
 816        cx.set_shared_state("The quick brown\nˇ\nfox").await;
 817        cx.simulate_shared_keystrokes(["v"]).await;
 818        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 819        cx.simulate_shared_keystrokes(["i", "w"]).await;
 820        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 821
 822        cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
 823            .await;
 824        cx.assert_binding_matches_all_exempted(
 825            ["v", "h", "i", "w"],
 826            WORD_LOCATIONS,
 827            ExemptionFeatures::NonEmptyVisualTextObjects,
 828        )
 829        .await;
 830        cx.assert_binding_matches_all_exempted(
 831            ["v", "l", "i", "w"],
 832            WORD_LOCATIONS,
 833            ExemptionFeatures::NonEmptyVisualTextObjects,
 834        )
 835        .await;
 836        cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
 837            .await;
 838
 839        cx.assert_binding_matches_all_exempted(
 840            ["v", "i", "h", "shift-w"],
 841            WORD_LOCATIONS,
 842            ExemptionFeatures::NonEmptyVisualTextObjects,
 843        )
 844        .await;
 845        cx.assert_binding_matches_all_exempted(
 846            ["v", "i", "l", "shift-w"],
 847            WORD_LOCATIONS,
 848            ExemptionFeatures::NonEmptyVisualTextObjects,
 849        )
 850        .await;
 851
 852        cx.assert_binding_matches_all_exempted(
 853            ["v", "a", "w"],
 854            WORD_LOCATIONS,
 855            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 856        )
 857        .await;
 858        cx.assert_binding_matches_all_exempted(
 859            ["v", "a", "shift-w"],
 860            WORD_LOCATIONS,
 861            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 862        )
 863        .await;
 864    }
 865
 866    const SENTENCE_EXAMPLES: &[&'static str] = &[
 867        "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
 868        indoc! {"
 869            ˇThe quick ˇbrownˇ
 870            fox jumps over
 871            the lazy doˇgˇ.ˇ ˇThe quick ˇ
 872            brown fox jumps over
 873        "},
 874        indoc! {"
 875            The quick brown fox jumps.
 876            Over the lazy dog
 877            ˇ
 878            ˇ
 879            ˇ  fox-jumpˇs over
 880            the lazy dog.ˇ
 881            ˇ
 882        "},
 883        r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
 884    ];
 885
 886    #[gpui::test]
 887    async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
 888        let mut cx = NeovimBackedTestContext::new(cx)
 889            .await
 890            .binding(["c", "i", "s"]);
 891        cx.add_initial_state_exemptions(
 892            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
 893            ExemptionFeatures::SentenceOnEmptyLines);
 894        cx.add_initial_state_exemptions(
 895            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
 896            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
 897        cx.add_initial_state_exemptions(
 898            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
 899            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
 900        for sentence_example in SENTENCE_EXAMPLES {
 901            cx.assert_all(sentence_example).await;
 902        }
 903
 904        let mut cx = cx.binding(["c", "a", "s"]);
 905        cx.add_initial_state_exemptions(
 906            "The quick brown?ˇ Fox Jumps! Over the lazy.",
 907            ExemptionFeatures::IncorrectLandingPosition,
 908        );
 909        cx.add_initial_state_exemptions(
 910            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
 911            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 912        );
 913
 914        for sentence_example in SENTENCE_EXAMPLES {
 915            cx.assert_all(sentence_example).await;
 916        }
 917    }
 918
 919    #[gpui::test]
 920    async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
 921        let mut cx = NeovimBackedTestContext::new(cx)
 922            .await
 923            .binding(["d", "i", "s"]);
 924        cx.add_initial_state_exemptions(
 925            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
 926            ExemptionFeatures::SentenceOnEmptyLines);
 927        cx.add_initial_state_exemptions(
 928            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
 929            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
 930        cx.add_initial_state_exemptions(
 931            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
 932            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
 933
 934        for sentence_example in SENTENCE_EXAMPLES {
 935            cx.assert_all(sentence_example).await;
 936        }
 937
 938        let mut cx = cx.binding(["d", "a", "s"]);
 939        cx.add_initial_state_exemptions(
 940            "The quick brown?ˇ Fox Jumps! Over the lazy.",
 941            ExemptionFeatures::IncorrectLandingPosition,
 942        );
 943        cx.add_initial_state_exemptions(
 944            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
 945            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
 946        );
 947
 948        for sentence_example in SENTENCE_EXAMPLES {
 949            cx.assert_all(sentence_example).await;
 950        }
 951    }
 952
 953    #[gpui::test]
 954    async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
 955        let mut cx = NeovimBackedTestContext::new(cx)
 956            .await
 957            .binding(["v", "i", "s"]);
 958        for sentence_example in SENTENCE_EXAMPLES {
 959            cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
 960                .await;
 961        }
 962
 963        let mut cx = cx.binding(["v", "a", "s"]);
 964        for sentence_example in SENTENCE_EXAMPLES {
 965            cx.assert_all_exempted(
 966                sentence_example,
 967                ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
 968            )
 969            .await;
 970        }
 971    }
 972
 973    // Test string with "`" for opening surrounders and "'" for closing surrounders
 974    const SURROUNDING_MARKER_STRING: &str = indoc! {"
 975        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
 976        'ˇfox juˇmps ovˇ`ˇer
 977        the ˇlazy dˇ'ˇoˇ`ˇg"};
 978
 979    const SURROUNDING_OBJECTS: &[(char, char)] = &[
 980        ('\'', '\''), // Quote
 981        ('`', '`'),   // Back Quote
 982        ('"', '"'),   // Double Quote
 983        ('(', ')'),   // Parentheses
 984        ('[', ']'),   // SquareBrackets
 985        ('{', '}'),   // CurlyBrackets
 986        ('<', '>'),   // AngleBrackets
 987    ];
 988
 989    #[gpui::test]
 990    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
 991        let mut cx = NeovimBackedTestContext::new(cx).await;
 992
 993        for (start, end) in SURROUNDING_OBJECTS {
 994            let marked_string = SURROUNDING_MARKER_STRING
 995                .replace('`', &start.to_string())
 996                .replace('\'', &end.to_string());
 997
 998            cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
 999                .await;
1000            cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
1001                .await;
1002            cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
1003                .await;
1004            cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
1005                .await;
1006        }
1007    }
1008    #[gpui::test]
1009    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1010        let mut cx = NeovimBackedTestContext::new(cx).await;
1011        cx.set_shared_wrap(12).await;
1012
1013        cx.set_shared_state(indoc! {
1014            "helˇlo \"world\"!"
1015        })
1016        .await;
1017        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1018        cx.assert_shared_state(indoc! {
1019            "hello \"«worldˇ»\"!"
1020        })
1021        .await;
1022
1023        cx.set_shared_state(indoc! {
1024            "hello \"wˇorld\"!"
1025        })
1026        .await;
1027        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1028        cx.assert_shared_state(indoc! {
1029            "hello \"«worldˇ»\"!"
1030        })
1031        .await;
1032
1033        cx.set_shared_state(indoc! {
1034            "hello \"wˇorld\"!"
1035        })
1036        .await;
1037        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1038        cx.assert_shared_state(indoc! {
1039            "hello« \"world\"ˇ»!"
1040        })
1041        .await;
1042
1043        cx.set_shared_state(indoc! {
1044            "hello \"wˇorld\" !"
1045        })
1046        .await;
1047        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1048        cx.assert_shared_state(indoc! {
1049            "hello «\"world\" ˇ»!"
1050        })
1051        .await;
1052
1053        cx.set_shared_state(indoc! {
1054            "hello \"wˇorld\"1055            goodbye"
1056        })
1057        .await;
1058        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1059        cx.assert_shared_state(indoc! {
1060            "hello «\"world\" ˇ»
1061            goodbye"
1062        })
1063        .await;
1064    }
1065
1066    #[gpui::test]
1067    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1068        let mut cx = NeovimBackedTestContext::new(cx).await;
1069
1070        cx.set_shared_state(indoc! {
1071            "func empty(a string) bool {
1072               if a == \"\" {
1073                  return true
1074               }
1075               ˇreturn false
1076            }"
1077        })
1078        .await;
1079        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1080        cx.assert_shared_state(indoc! {"
1081            func empty(a string) bool {
1082            «   if a == \"\" {
1083                  return true
1084               }
1085               return false
1086            ˇ»}"})
1087            .await;
1088        cx.set_shared_state(indoc! {
1089            "func empty(a string) bool {
1090                 if a == \"\" {
1091                     ˇreturn true
1092                 }
1093                 return false
1094            }"
1095        })
1096        .await;
1097        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1098        cx.assert_shared_state(indoc! {"
1099            func empty(a string) bool {
1100                 if a == \"\" {
1101            «         return true
1102            ˇ»     }
1103                 return false
1104            }"})
1105            .await;
1106
1107        cx.set_shared_state(indoc! {
1108            "func empty(a string) bool {
1109                 if a == \"\" ˇ{
1110                     return true
1111                 }
1112                 return false
1113            }"
1114        })
1115        .await;
1116        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1117        cx.assert_shared_state(indoc! {"
1118            func empty(a string) bool {
1119                 if a == \"\" {
1120            «         return true
1121            ˇ»     }
1122                 return false
1123            }"})
1124            .await;
1125    }
1126
1127    #[gpui::test]
1128    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1129        let mut cx = VimTestContext::new(cx, true).await;
1130        cx.set_state(
1131            indoc! {"
1132            fn boop() {
1133                baz(ˇ|a, b| { bar(|j, k| { })})
1134            }"
1135            },
1136            Mode::Normal,
1137        );
1138        cx.simulate_keystrokes(["c", "i", "|"]);
1139        cx.assert_state(
1140            indoc! {"
1141            fn boop() {
1142                baz(|ˇ| { bar(|j, k| { })})
1143            }"
1144            },
1145            Mode::Insert,
1146        );
1147        cx.simulate_keystrokes(["escape", "1", "8", "|"]);
1148        cx.assert_state(
1149            indoc! {"
1150            fn boop() {
1151                baz(|| { bar(ˇ|j, k| { })})
1152            }"
1153            },
1154            Mode::Normal,
1155        );
1156
1157        cx.simulate_keystrokes(["v", "a", "|"]);
1158        cx.assert_state(
1159            indoc! {"
1160            fn boop() {
1161                baz(|| { bar(«|j, k| ˇ»{ })})
1162            }"
1163            },
1164            Mode::Visual,
1165        );
1166    }
1167
1168    #[gpui::test]
1169    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1170        let mut cx = VimTestContext::new(cx, true).await;
1171
1172        // Generic arguments
1173        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1174        cx.simulate_keystrokes(["v", "i", "a"]);
1175        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1176
1177        // Function arguments
1178        cx.set_state(
1179            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1180            Mode::Normal,
1181        );
1182        cx.simulate_keystrokes(["d", "a", "a"]);
1183        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1184
1185        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1186        cx.simulate_keystrokes(["v", "a", "a"]);
1187        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1188
1189        // Tuple, vec, and array arguments
1190        cx.set_state(
1191            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1192            Mode::Normal,
1193        );
1194        cx.simulate_keystrokes(["c", "i", "a"]);
1195        cx.assert_state(
1196            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1197            Mode::Insert,
1198        );
1199
1200        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1201        cx.simulate_keystrokes(["c", "a", "a"]);
1202        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1203
1204        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1205        cx.simulate_keystrokes(["c", "i", "a"]);
1206        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1207
1208        cx.set_state(
1209            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1210            Mode::Normal,
1211        );
1212        cx.simulate_keystrokes(["c", "a", "a"]);
1213        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1214
1215        // Cursor immediately before / after brackets
1216        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1217        cx.simulate_keystrokes(["v", "i", "a"]);
1218        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1219
1220        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1221        cx.simulate_keystrokes(["v", "i", "a"]);
1222        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1223    }
1224
1225    #[gpui::test]
1226    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1227        let mut cx = NeovimBackedTestContext::new(cx).await;
1228
1229        for (start, end) in SURROUNDING_OBJECTS {
1230            let marked_string = SURROUNDING_MARKER_STRING
1231                .replace('`', &start.to_string())
1232                .replace('\'', &end.to_string());
1233
1234            cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
1235                .await;
1236            cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
1237                .await;
1238            cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
1239                .await;
1240            cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
1241                .await;
1242        }
1243    }
1244}