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