object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation,
   5    visual::visual_object, Vim,
   6};
   7use editor::{
   8    display_map::{DisplaySnapshot, ToDisplayPoint},
   9    movement::{self, FindRange},
  10    Bias, DisplayPoint,
  11};
  12use gpui::{actions, impl_actions, ViewContext, WindowContext};
  13use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
  14use serde::Deserialize;
  15use workspace::Workspace;
  16
  17#[derive(Copy, Clone, Debug, PartialEq)]
  18pub enum Object {
  19    Word { ignore_punctuation: bool },
  20    Sentence,
  21    Paragraph,
  22    Quotes,
  23    BackQuotes,
  24    DoubleQuotes,
  25    VerticalBars,
  26    Parentheses,
  27    SquareBrackets,
  28    CurlyBrackets,
  29    AngleBrackets,
  30    Argument,
  31    Tag,
  32}
  33
  34#[derive(Clone, Deserialize, PartialEq)]
  35#[serde(rename_all = "camelCase")]
  36struct Word {
  37    #[serde(default)]
  38    ignore_punctuation: bool,
  39}
  40
  41impl_actions!(vim, [Word]);
  42
  43actions!(
  44    vim,
  45    [
  46        Sentence,
  47        Paragraph,
  48        Quotes,
  49        BackQuotes,
  50        DoubleQuotes,
  51        VerticalBars,
  52        Parentheses,
  53        SquareBrackets,
  54        CurlyBrackets,
  55        AngleBrackets,
  56        Argument,
  57        Tag
  58    ]
  59);
  60
  61pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  62    workspace.register_action(
  63        |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
  64            object(Object::Word { ignore_punctuation }, cx)
  65        },
  66    );
  67    workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx));
  68    workspace
  69        .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
  70    workspace
  71        .register_action(|_: &mut Workspace, _: &Paragraph, cx: _| object(Object::Paragraph, cx));
  72    workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
  73    workspace
  74        .register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
  75    workspace.register_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| {
  76        object(Object::DoubleQuotes, cx)
  77    });
  78    workspace.register_action(|_: &mut Workspace, _: &Parentheses, cx: _| {
  79        object(Object::Parentheses, cx)
  80    });
  81    workspace.register_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
  82        object(Object::SquareBrackets, cx)
  83    });
  84    workspace.register_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| {
  85        object(Object::CurlyBrackets, cx)
  86    });
  87    workspace.register_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| {
  88        object(Object::AngleBrackets, cx)
  89    });
  90    workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
  91        object(Object::VerticalBars, cx)
  92    });
  93    workspace
  94        .register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx));
  95}
  96
  97fn object(object: Object, cx: &mut WindowContext) {
  98    match Vim::read(cx).state().mode {
  99        Mode::Normal => normal_object(object, cx),
 100        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
 101        Mode::Insert | Mode::Replace => {
 102            // Shouldn't execute a text object in insert mode. Ignoring
 103        }
 104    }
 105}
 106
 107impl Object {
 108    pub fn is_multiline(self) -> bool {
 109        match self {
 110            Object::Word { .. }
 111            | Object::Quotes
 112            | Object::BackQuotes
 113            | Object::VerticalBars
 114            | Object::DoubleQuotes => false,
 115            Object::Sentence
 116            | Object::Paragraph
 117            | Object::Parentheses
 118            | Object::Tag
 119            | Object::AngleBrackets
 120            | Object::CurlyBrackets
 121            | Object::SquareBrackets
 122            | Object::Argument => true,
 123        }
 124    }
 125
 126    pub fn always_expands_both_ways(self) -> bool {
 127        match self {
 128            Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false,
 129            Object::Quotes
 130            | Object::BackQuotes
 131            | Object::DoubleQuotes
 132            | Object::VerticalBars
 133            | Object::Parentheses
 134            | Object::SquareBrackets
 135            | Object::Tag
 136            | Object::CurlyBrackets
 137            | Object::AngleBrackets => true,
 138        }
 139    }
 140
 141    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
 142        match self {
 143            Object::Word { .. }
 144            | Object::Sentence
 145            | Object::Quotes
 146            | Object::BackQuotes
 147            | Object::DoubleQuotes => {
 148                if current_mode == Mode::VisualBlock {
 149                    Mode::VisualBlock
 150                } else {
 151                    Mode::Visual
 152                }
 153            }
 154            Object::Parentheses
 155            | Object::SquareBrackets
 156            | Object::CurlyBrackets
 157            | Object::AngleBrackets
 158            | Object::VerticalBars
 159            | Object::Tag
 160            | Object::Argument => Mode::Visual,
 161            Object::Paragraph => Mode::VisualLine,
 162        }
 163    }
 164
 165    pub fn range(
 166        self,
 167        map: &DisplaySnapshot,
 168        selection: Selection<DisplayPoint>,
 169        around: bool,
 170    ) -> Option<Range<DisplayPoint>> {
 171        let relative_to = selection.head();
 172        match self {
 173            Object::Word { ignore_punctuation } => {
 174                if around {
 175                    around_word(map, relative_to, ignore_punctuation)
 176                } else {
 177                    in_word(map, relative_to, ignore_punctuation)
 178                }
 179            }
 180            Object::Sentence => sentence(map, relative_to, around),
 181            Object::Paragraph => paragraph(map, relative_to, around),
 182            Object::Quotes => {
 183                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 184            }
 185            Object::BackQuotes => {
 186                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 187            }
 188            Object::DoubleQuotes => {
 189                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 190            }
 191            Object::VerticalBars => {
 192                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 193            }
 194            Object::Parentheses => {
 195                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 196            }
 197            Object::Tag => surrounding_html_tag(map, selection, around),
 198            Object::SquareBrackets => {
 199                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 200            }
 201            Object::CurlyBrackets => {
 202                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 203            }
 204            Object::AngleBrackets => {
 205                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 206            }
 207            Object::Argument => argument(map, relative_to, around),
 208        }
 209    }
 210
 211    pub fn expand_selection(
 212        self,
 213        map: &DisplaySnapshot,
 214        selection: &mut Selection<DisplayPoint>,
 215        around: bool,
 216    ) -> bool {
 217        if let Some(range) = self.range(map, selection.clone(), around) {
 218            selection.start = range.start;
 219            selection.end = range.end;
 220            true
 221        } else {
 222            false
 223        }
 224    }
 225}
 226
 227/// Returns a range that surrounds the word `relative_to` is in.
 228///
 229/// If `relative_to` is at the start of a word, return the word.
 230/// If `relative_to` is between words, return the space between.
 231fn in_word(
 232    map: &DisplaySnapshot,
 233    relative_to: DisplayPoint,
 234    ignore_punctuation: bool,
 235) -> Option<Range<DisplayPoint>> {
 236    // Use motion::right so that we consider the character under the cursor when looking for the start
 237    let scope = map
 238        .buffer_snapshot
 239        .language_scope_at(relative_to.to_point(map));
 240    let start = movement::find_preceding_boundary_display_point(
 241        map,
 242        right(map, relative_to, 1),
 243        movement::FindRange::SingleLine,
 244        |left, right| {
 245            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 246                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 247        },
 248    );
 249
 250    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 251        coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 252            != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 253    });
 254
 255    Some(start..end)
 256}
 257
 258fn surrounding_html_tag(
 259    map: &DisplaySnapshot,
 260    selection: Selection<DisplayPoint>,
 261    around: bool,
 262) -> Option<Range<DisplayPoint>> {
 263    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 264        chars
 265            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 266            .collect()
 267    }
 268    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 269        if Some('<') != chars.next() {
 270            return None;
 271        }
 272        Some(read_tag(chars))
 273    }
 274    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 275        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 276            return None;
 277        }
 278        Some(read_tag(chars))
 279    }
 280
 281    let snapshot = &map.buffer_snapshot;
 282    let offset = selection.head().to_offset(map, Bias::Left);
 283    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 284    let buffer = excerpt.buffer();
 285    let offset = excerpt.map_offset_to_buffer(offset);
 286
 287    // Find the most closest to current offset
 288    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 289    let mut last_child_node = cursor.node();
 290    while cursor.goto_first_child_for_byte(offset).is_some() {
 291        last_child_node = cursor.node();
 292    }
 293
 294    let mut last_child_node = Some(last_child_node);
 295    while let Some(cur_node) = last_child_node {
 296        if cur_node.child_count() >= 2 {
 297            let first_child = cur_node.child(0);
 298            let last_child = cur_node.child(cur_node.child_count() - 1);
 299            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 300                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 301                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 302                // It needs to be handled differently according to the selection length
 303                let is_valid = if selection.end.to_offset(map, Bias::Left)
 304                    - selection.start.to_offset(map, Bias::Left)
 305                    <= 1
 306                {
 307                    offset <= last_child.end_byte()
 308                } else {
 309                    selection.start.to_offset(map, Bias::Left) >= first_child.start_byte()
 310                        && selection.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
 311                };
 312                if open_tag.is_some() && open_tag == close_tag && is_valid {
 313                    let range = if around {
 314                        first_child.byte_range().start..last_child.byte_range().end
 315                    } else {
 316                        first_child.byte_range().end..last_child.byte_range().start
 317                    };
 318                    if excerpt.contains_buffer_range(range.clone()) {
 319                        let result = excerpt.map_range_from_buffer(range);
 320                        return Some(
 321                            result.start.to_display_point(map)..result.end.to_display_point(map),
 322                        );
 323                    }
 324                }
 325            }
 326        }
 327        last_child_node = cur_node.parent();
 328    }
 329    None
 330}
 331
 332/// Returns a range that surrounds the word and following whitespace
 333/// relative_to is in.
 334///
 335/// If `relative_to` is at the start of a word, return the word and following whitespace.
 336/// If `relative_to` is between words, return the whitespace back and the following word.
 337///
 338/// if in word
 339///   delete that word
 340///   if there is whitespace following the word, delete that as well
 341///   otherwise, delete any preceding whitespace
 342/// otherwise
 343///   delete whitespace around cursor
 344///   delete word following the cursor
 345fn around_word(
 346    map: &DisplaySnapshot,
 347    relative_to: DisplayPoint,
 348    ignore_punctuation: bool,
 349) -> Option<Range<DisplayPoint>> {
 350    let scope = map
 351        .buffer_snapshot
 352        .language_scope_at(relative_to.to_point(map));
 353    let in_word = map
 354        .chars_at(relative_to)
 355        .next()
 356        .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
 357        .unwrap_or(false);
 358
 359    if in_word {
 360        around_containing_word(map, relative_to, ignore_punctuation)
 361    } else {
 362        around_next_word(map, relative_to, ignore_punctuation)
 363    }
 364}
 365
 366fn around_containing_word(
 367    map: &DisplaySnapshot,
 368    relative_to: DisplayPoint,
 369    ignore_punctuation: bool,
 370) -> Option<Range<DisplayPoint>> {
 371    in_word(map, relative_to, ignore_punctuation)
 372        .map(|range| expand_to_include_whitespace(map, range, true))
 373}
 374
 375fn around_next_word(
 376    map: &DisplaySnapshot,
 377    relative_to: DisplayPoint,
 378    ignore_punctuation: bool,
 379) -> Option<Range<DisplayPoint>> {
 380    let scope = map
 381        .buffer_snapshot
 382        .language_scope_at(relative_to.to_point(map));
 383    // Get the start of the word
 384    let start = movement::find_preceding_boundary_display_point(
 385        map,
 386        right(map, relative_to, 1),
 387        FindRange::SingleLine,
 388        |left, right| {
 389            coerce_punctuation(char_kind(&scope, left), ignore_punctuation)
 390                != coerce_punctuation(char_kind(&scope, right), ignore_punctuation)
 391        },
 392    );
 393
 394    let mut word_found = false;
 395    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 396        let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 397        let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 398
 399        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 400
 401        if right_kind != CharKind::Whitespace {
 402            word_found = true;
 403        }
 404
 405        found
 406    });
 407
 408    Some(start..end)
 409}
 410
 411fn argument(
 412    map: &DisplaySnapshot,
 413    relative_to: DisplayPoint,
 414    around: bool,
 415) -> Option<Range<DisplayPoint>> {
 416    let snapshot = &map.buffer_snapshot;
 417    let offset = relative_to.to_offset(map, Bias::Left);
 418
 419    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 420    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 421    let buffer = excerpt.buffer();
 422
 423    fn comma_delimited_range_at(
 424        buffer: &BufferSnapshot,
 425        mut offset: usize,
 426        include_comma: bool,
 427    ) -> Option<Range<usize>> {
 428        // Seek to the first non-whitespace character
 429        offset += buffer
 430            .chars_at(offset)
 431            .take_while(|c| c.is_whitespace())
 432            .map(char::len_utf8)
 433            .sum::<usize>();
 434
 435        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 436            // Filter out empty ranges
 437            if open.end == close.start {
 438                return false;
 439            }
 440
 441            // If the cursor is outside the brackets, ignore them
 442            if open.start == offset || close.end == offset {
 443                return false;
 444            }
 445
 446            // TODO: Is there any better way to filter out string brackets?
 447            // Used to filter out string brackets
 448            return matches!(
 449                buffer.chars_at(open.start).next(),
 450                Some('(' | '[' | '{' | '<' | '|')
 451            );
 452        };
 453
 454        // Find the brackets containing the cursor
 455        let (open_bracket, close_bracket) =
 456            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 457
 458        let inner_bracket_range = open_bracket.end..close_bracket.start;
 459
 460        let layer = buffer.syntax_layer_at(offset)?;
 461        let node = layer.node();
 462        let mut cursor = node.walk();
 463
 464        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 465        let mut parent_covers_bracket_range = false;
 466        loop {
 467            let node = cursor.node();
 468            let range = node.byte_range();
 469            let covers_bracket_range =
 470                range.start == open_bracket.start && range.end == close_bracket.end;
 471            if parent_covers_bracket_range && !covers_bracket_range {
 472                break;
 473            }
 474            parent_covers_bracket_range = covers_bracket_range;
 475
 476            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 477            if cursor.goto_first_child_for_byte(offset).is_none() {
 478                return None;
 479            }
 480        }
 481
 482        let mut argument_node = cursor.node();
 483
 484        // If the child node is the open bracket, move to the next sibling.
 485        if argument_node.byte_range() == open_bracket {
 486            if !cursor.goto_next_sibling() {
 487                return Some(inner_bracket_range);
 488            }
 489            argument_node = cursor.node();
 490        }
 491        // While the child node is the close bracket or a comma, move to the previous sibling
 492        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 493            if !cursor.goto_previous_sibling() {
 494                return Some(inner_bracket_range);
 495            }
 496            argument_node = cursor.node();
 497            if argument_node.byte_range() == open_bracket {
 498                return Some(inner_bracket_range);
 499            }
 500        }
 501
 502        // The start and end of the argument range, defaulting to the start and end of the argument node
 503        let mut start = argument_node.start_byte();
 504        let mut end = argument_node.end_byte();
 505
 506        let mut needs_surrounding_comma = include_comma;
 507
 508        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 509        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 510        while cursor.goto_previous_sibling() {
 511            let prev = cursor.node();
 512
 513            if prev.start_byte() < open_bracket.end {
 514                start = open_bracket.end;
 515                break;
 516            } else if prev.kind() == "," {
 517                if needs_surrounding_comma {
 518                    start = prev.start_byte();
 519                    needs_surrounding_comma = false;
 520                }
 521                break;
 522            } else if prev.start_byte() < start {
 523                start = prev.start_byte();
 524            }
 525        }
 526
 527        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 528        while cursor.goto_next_sibling() {
 529            let next = cursor.node();
 530
 531            if next.end_byte() > close_bracket.start {
 532                end = close_bracket.start;
 533                break;
 534            } else if next.kind() == "," {
 535                if needs_surrounding_comma {
 536                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 537                    if let Some(next_arg) = next.next_sibling() {
 538                        end = next_arg.start_byte();
 539                    } else {
 540                        end = next.end_byte();
 541                    }
 542                }
 543                break;
 544            } else if next.end_byte() > end {
 545                end = next.end_byte();
 546            }
 547        }
 548
 549        Some(start..end)
 550    }
 551
 552    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 553
 554    if excerpt.contains_buffer_range(result.clone()) {
 555        let result = excerpt.map_range_from_buffer(result);
 556        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 557    } else {
 558        None
 559    }
 560}
 561
 562fn sentence(
 563    map: &DisplaySnapshot,
 564    relative_to: DisplayPoint,
 565    around: bool,
 566) -> Option<Range<DisplayPoint>> {
 567    let mut start = None;
 568    let mut previous_end = relative_to;
 569
 570    let mut chars = map.chars_at(relative_to).peekable();
 571
 572    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 573    for (char, point) in chars
 574        .peek()
 575        .cloned()
 576        .into_iter()
 577        .chain(map.reverse_chars_at(relative_to))
 578    {
 579        if is_sentence_end(map, point) {
 580            break;
 581        }
 582
 583        if is_possible_sentence_start(char) {
 584            start = Some(point);
 585        }
 586
 587        previous_end = point;
 588    }
 589
 590    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 591    let mut end = relative_to;
 592    for (char, point) in chars {
 593        if start.is_none() && is_possible_sentence_start(char) {
 594            if around {
 595                start = Some(point);
 596                continue;
 597            } else {
 598                end = point;
 599                break;
 600            }
 601        }
 602
 603        end = point;
 604        *end.column_mut() += char.len_utf8() as u32;
 605        end = map.clip_point(end, Bias::Left);
 606
 607        if is_sentence_end(map, end) {
 608            break;
 609        }
 610    }
 611
 612    let mut range = start.unwrap_or(previous_end)..end;
 613    if around {
 614        range = expand_to_include_whitespace(map, range, false);
 615    }
 616
 617    Some(range)
 618}
 619
 620fn is_possible_sentence_start(character: char) -> bool {
 621    !character.is_whitespace() && character != '.'
 622}
 623
 624const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 625const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 626const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 627fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
 628    let mut next_chars = map.chars_at(point).peekable();
 629    if let Some((char, _)) = next_chars.next() {
 630        // We are at a double newline. This position is a sentence end.
 631        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 632            return true;
 633        }
 634
 635        // The next text is not a valid whitespace. This is not a sentence end
 636        if !SENTENCE_END_WHITESPACE.contains(&char) {
 637            return false;
 638        }
 639    }
 640
 641    for (char, _) in map.reverse_chars_at(point) {
 642        if SENTENCE_END_PUNCTUATION.contains(&char) {
 643            return true;
 644        }
 645
 646        if !SENTENCE_END_FILLERS.contains(&char) {
 647            return false;
 648        }
 649    }
 650
 651    return false;
 652}
 653
 654/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 655/// whitespace to the end first and falls back to the start if there was none.
 656fn expand_to_include_whitespace(
 657    map: &DisplaySnapshot,
 658    mut range: Range<DisplayPoint>,
 659    stop_at_newline: bool,
 660) -> Range<DisplayPoint> {
 661    let mut whitespace_included = false;
 662
 663    let mut chars = map.chars_at(range.end).peekable();
 664    while let Some((char, point)) = chars.next() {
 665        if char == '\n' && stop_at_newline {
 666            break;
 667        }
 668
 669        if char.is_whitespace() {
 670            // Set end to the next display_point or the character position after the current display_point
 671            range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
 672                let mut end = point;
 673                *end.column_mut() += char.len_utf8() as u32;
 674                map.clip_point(end, Bias::Left)
 675            });
 676
 677            if char != '\n' {
 678                whitespace_included = true;
 679            }
 680        } else {
 681            // Found non whitespace. Quit out.
 682            break;
 683        }
 684    }
 685
 686    if !whitespace_included {
 687        for (char, point) in map.reverse_chars_at(range.start) {
 688            if char == '\n' && stop_at_newline {
 689                break;
 690            }
 691
 692            if !char.is_whitespace() {
 693                break;
 694            }
 695
 696            range.start = point;
 697        }
 698    }
 699
 700    range
 701}
 702
 703/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
 704/// where `relative_to` is in. If `around`, principally returns the range ending
 705/// at the end of the next paragraph.
 706///
 707/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
 708/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
 709/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
 710/// the trailing newline is not subject to subsequent operations).
 711///
 712/// Edge cases:
 713/// - If `around` and if the current paragraph is the last paragraph of the
 714///   file and is blank, then the selection results in an error.
 715/// - If `around` and if the current paragraph is the last paragraph of the
 716///   file and is not blank, then the returned range starts at the start of the
 717///   previous paragraph, if it exists.
 718fn paragraph(
 719    map: &DisplaySnapshot,
 720    relative_to: DisplayPoint,
 721    around: bool,
 722) -> Option<Range<DisplayPoint>> {
 723    let mut paragraph_start = start_of_paragraph(map, relative_to);
 724    let mut paragraph_end = end_of_paragraph(map, relative_to);
 725
 726    let paragraph_end_row = paragraph_end.row();
 727    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
 728    let point = relative_to.to_point(map);
 729    let current_line_is_empty = map.buffer_snapshot.is_line_blank(point.row);
 730
 731    if around {
 732        if paragraph_ends_with_eof {
 733            if current_line_is_empty {
 734                return None;
 735            }
 736
 737            let paragraph_start_row = paragraph_start.row();
 738            if paragraph_start_row != 0 {
 739                let previous_paragraph_last_line_start =
 740                    Point::new(paragraph_start_row - 1, 0).to_display_point(map);
 741                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
 742            }
 743        } else {
 744            let next_paragraph_start = Point::new(paragraph_end_row + 1, 0).to_display_point(map);
 745            paragraph_end = end_of_paragraph(map, next_paragraph_start);
 746        }
 747    }
 748
 749    let range = paragraph_start..paragraph_end;
 750    Some(range)
 751}
 752
 753/// Returns a position of the start of the current paragraph, where a paragraph
 754/// is defined as a run of non-blank lines or a run of blank lines.
 755pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 756    let point = display_point.to_point(map);
 757    if point.row == 0 {
 758        return DisplayPoint::zero();
 759    }
 760
 761    let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row);
 762
 763    for row in (0..point.row).rev() {
 764        let blank = map.buffer_snapshot.is_line_blank(row);
 765        if blank != is_current_line_blank {
 766            return Point::new(row + 1, 0).to_display_point(map);
 767        }
 768    }
 769
 770    DisplayPoint::zero()
 771}
 772
 773/// Returns a position of the end of the current paragraph, where a paragraph
 774/// is defined as a run of non-blank lines or a run of blank lines.
 775/// The trailing newline is excluded from the paragraph.
 776pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 777    let point = display_point.to_point(map);
 778    if point.row == map.max_buffer_row() {
 779        return map.max_point();
 780    }
 781
 782    let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row);
 783
 784    for row in point.row + 1..map.max_buffer_row() + 1 {
 785        let blank = map.buffer_snapshot.is_line_blank(row);
 786        if blank != is_current_line_blank {
 787            let previous_row = row - 1;
 788            return Point::new(previous_row, map.buffer_snapshot.line_len(previous_row))
 789                .to_display_point(map);
 790        }
 791    }
 792
 793    map.max_point()
 794}
 795
 796fn surrounding_markers(
 797    map: &DisplaySnapshot,
 798    relative_to: DisplayPoint,
 799    around: bool,
 800    search_across_lines: bool,
 801    open_marker: char,
 802    close_marker: char,
 803) -> Option<Range<DisplayPoint>> {
 804    let point = relative_to.to_offset(map, Bias::Left);
 805
 806    let mut matched_closes = 0;
 807    let mut opening = None;
 808
 809    if let Some((ch, range)) = movement::chars_after(map, point).next() {
 810        if ch == open_marker {
 811            if open_marker == close_marker {
 812                let mut total = 0;
 813                for (ch, _) in movement::chars_before(map, point) {
 814                    if ch == '\n' {
 815                        break;
 816                    }
 817                    if ch == open_marker {
 818                        total += 1;
 819                    }
 820                }
 821                if total % 2 == 0 {
 822                    opening = Some(range)
 823                }
 824            } else {
 825                opening = Some(range)
 826            }
 827        }
 828    }
 829
 830    if opening.is_none() {
 831        for (ch, range) in movement::chars_before(map, point) {
 832            if ch == '\n' && !search_across_lines {
 833                break;
 834            }
 835
 836            if ch == open_marker {
 837                if matched_closes == 0 {
 838                    opening = Some(range);
 839                    break;
 840                }
 841                matched_closes -= 1;
 842            } else if ch == close_marker {
 843                matched_closes += 1
 844            }
 845        }
 846    }
 847
 848    if opening.is_none() {
 849        for (ch, range) in movement::chars_after(map, point) {
 850            if ch == open_marker {
 851                opening = Some(range);
 852                break;
 853            } else if ch == close_marker {
 854                break;
 855            }
 856        }
 857    }
 858
 859    let Some(mut opening) = opening else {
 860        return None;
 861    };
 862
 863    let mut matched_opens = 0;
 864    let mut closing = None;
 865
 866    for (ch, range) in movement::chars_after(map, opening.end) {
 867        if ch == '\n' && !search_across_lines {
 868            break;
 869        }
 870
 871        if ch == close_marker {
 872            if matched_opens == 0 {
 873                closing = Some(range);
 874                break;
 875            }
 876            matched_opens -= 1;
 877        } else if ch == open_marker {
 878            matched_opens += 1;
 879        }
 880    }
 881
 882    let Some(mut closing) = closing else {
 883        return None;
 884    };
 885
 886    if around && !search_across_lines {
 887        let mut found = false;
 888
 889        for (ch, range) in movement::chars_after(map, closing.end) {
 890            if ch.is_whitespace() && ch != '\n' {
 891                found = true;
 892                closing.end = range.end;
 893            } else {
 894                break;
 895            }
 896        }
 897
 898        if !found {
 899            for (ch, range) in movement::chars_before(map, opening.start) {
 900                if ch.is_whitespace() && ch != '\n' {
 901                    opening.start = range.start
 902                } else {
 903                    break;
 904                }
 905            }
 906        }
 907    }
 908
 909    if !around && search_across_lines {
 910        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
 911            if ch == '\n' {
 912                opening.end = range.end
 913            }
 914        }
 915
 916        for (ch, range) in movement::chars_before(map, closing.start) {
 917            if !ch.is_whitespace() {
 918                break;
 919            }
 920            if ch != '\n' {
 921                closing.start = range.start
 922            }
 923        }
 924    }
 925
 926    let result = if around {
 927        opening.start..closing.end
 928    } else {
 929        opening.end..closing.start
 930    };
 931
 932    Some(
 933        map.clip_point(result.start.to_display_point(map), Bias::Left)
 934            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
 935    )
 936}
 937
 938#[cfg(test)]
 939mod test {
 940    use indoc::indoc;
 941
 942    use crate::{
 943        state::Mode,
 944        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
 945    };
 946
 947    const WORD_LOCATIONS: &str = indoc! {"
 948        The quick ˇbrowˇnˇ•••
 949        fox ˇjuˇmpsˇ over
 950        the lazy dogˇ••
 951        ˇ
 952        ˇ
 953        ˇ
 954        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
 955        ˇ••
 956        ˇ••
 957        ˇ  fox-jumpˇs over
 958        the lazy dogˇ•
 959        ˇ
 960        "
 961    };
 962
 963    #[gpui::test]
 964    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
 965        let mut cx = NeovimBackedTestContext::new(cx).await;
 966
 967        cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
 968            .await;
 969        cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
 970            .await;
 971        cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
 972            .await;
 973        cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
 974            .await;
 975    }
 976
 977    #[gpui::test]
 978    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
 979        let mut cx = NeovimBackedTestContext::new(cx).await;
 980
 981        cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
 982            .await;
 983        cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
 984            .await;
 985        cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
 986            .await;
 987        cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
 988            .await;
 989    }
 990
 991    #[gpui::test]
 992    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
 993        let mut cx = NeovimBackedTestContext::new(cx).await;
 994
 995        /*
 996                cx.set_shared_state("The quick ˇbrown\nfox").await;
 997                cx.simulate_shared_keystrokes(["v"]).await;
 998                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
 999                cx.simulate_shared_keystrokes(["i", "w"]).await;
1000                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1001        */
1002        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1003        cx.simulate_shared_keystrokes(["v"]).await;
1004        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
1005        cx.simulate_shared_keystrokes(["i", "w"]).await;
1006        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
1007
1008        cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
1009            .await;
1010        cx.assert_binding_matches_all_exempted(
1011            ["v", "h", "i", "w"],
1012            WORD_LOCATIONS,
1013            ExemptionFeatures::NonEmptyVisualTextObjects,
1014        )
1015        .await;
1016        cx.assert_binding_matches_all_exempted(
1017            ["v", "l", "i", "w"],
1018            WORD_LOCATIONS,
1019            ExemptionFeatures::NonEmptyVisualTextObjects,
1020        )
1021        .await;
1022        cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
1023            .await;
1024
1025        cx.assert_binding_matches_all_exempted(
1026            ["v", "i", "h", "shift-w"],
1027            WORD_LOCATIONS,
1028            ExemptionFeatures::NonEmptyVisualTextObjects,
1029        )
1030        .await;
1031        cx.assert_binding_matches_all_exempted(
1032            ["v", "i", "l", "shift-w"],
1033            WORD_LOCATIONS,
1034            ExemptionFeatures::NonEmptyVisualTextObjects,
1035        )
1036        .await;
1037
1038        cx.assert_binding_matches_all_exempted(
1039            ["v", "a", "w"],
1040            WORD_LOCATIONS,
1041            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1042        )
1043        .await;
1044        cx.assert_binding_matches_all_exempted(
1045            ["v", "a", "shift-w"],
1046            WORD_LOCATIONS,
1047            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1048        )
1049        .await;
1050    }
1051
1052    const SENTENCE_EXAMPLES: &[&'static str] = &[
1053        "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
1054        indoc! {"
1055            ˇThe quick ˇbrownˇ
1056            fox jumps over
1057            the lazy doˇgˇ.ˇ ˇThe quick ˇ
1058            brown fox jumps over
1059        "},
1060        indoc! {"
1061            The quick brown fox jumps.
1062            Over the lazy dog
1063            ˇ
1064            ˇ
1065            ˇ  fox-jumpˇs over
1066            the lazy dog.ˇ
1067            ˇ
1068        "},
1069        r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
1070    ];
1071
1072    #[gpui::test]
1073    async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
1074        let mut cx = NeovimBackedTestContext::new(cx)
1075            .await
1076            .binding(["c", "i", "s"]);
1077        cx.add_initial_state_exemptions(
1078            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
1079            ExemptionFeatures::SentenceOnEmptyLines);
1080        cx.add_initial_state_exemptions(
1081            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
1082            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
1083        cx.add_initial_state_exemptions(
1084            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
1085            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
1086        for sentence_example in SENTENCE_EXAMPLES {
1087            cx.assert_all(sentence_example).await;
1088        }
1089
1090        let mut cx = cx.binding(["c", "a", "s"]);
1091        cx.add_initial_state_exemptions(
1092            "The quick brown?ˇ Fox Jumps! Over the lazy.",
1093            ExemptionFeatures::IncorrectLandingPosition,
1094        );
1095        cx.add_initial_state_exemptions(
1096            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
1097            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1098        );
1099
1100        for sentence_example in SENTENCE_EXAMPLES {
1101            cx.assert_all(sentence_example).await;
1102        }
1103    }
1104
1105    #[gpui::test]
1106    async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
1107        let mut cx = NeovimBackedTestContext::new(cx)
1108            .await
1109            .binding(["d", "i", "s"]);
1110        cx.add_initial_state_exemptions(
1111            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
1112            ExemptionFeatures::SentenceOnEmptyLines);
1113        cx.add_initial_state_exemptions(
1114            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
1115            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
1116        cx.add_initial_state_exemptions(
1117            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
1118            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
1119
1120        for sentence_example in SENTENCE_EXAMPLES {
1121            cx.assert_all(sentence_example).await;
1122        }
1123
1124        let mut cx = cx.binding(["d", "a", "s"]);
1125        cx.add_initial_state_exemptions(
1126            "The quick brown?ˇ Fox Jumps! Over the lazy.",
1127            ExemptionFeatures::IncorrectLandingPosition,
1128        );
1129        cx.add_initial_state_exemptions(
1130            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
1131            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
1132        );
1133
1134        for sentence_example in SENTENCE_EXAMPLES {
1135            cx.assert_all(sentence_example).await;
1136        }
1137    }
1138
1139    #[gpui::test]
1140    async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
1141        let mut cx = NeovimBackedTestContext::new(cx)
1142            .await
1143            .binding(["v", "i", "s"]);
1144        for sentence_example in SENTENCE_EXAMPLES {
1145            cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
1146                .await;
1147        }
1148
1149        let mut cx = cx.binding(["v", "a", "s"]);
1150        for sentence_example in SENTENCE_EXAMPLES {
1151            cx.assert_all_exempted(
1152                sentence_example,
1153                ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
1154            )
1155            .await;
1156        }
1157    }
1158
1159    const PARAGRAPH_EXAMPLES: &[&'static str] = &[
1160        // Single line
1161        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1162        // Multiple lines without empty lines
1163        indoc! {"
1164            ˇThe quick brownˇ
1165            ˇfox jumps overˇ
1166            the lazy dog.ˇ
1167        "},
1168        // Heading blank paragraph and trailing normal paragraph
1169        indoc! {"
1170            ˇ
1171            ˇ
1172            ˇThe quick brown fox jumps
1173            ˇover the lazy dog.
1174            ˇ
1175            ˇ
1176            ˇThe quick brown fox jumpsˇ
1177            ˇover the lazy dog.ˇ
1178        "},
1179        // Inserted blank paragraph and trailing blank paragraph
1180        indoc! {"
1181            ˇThe quick brown fox jumps
1182            ˇover the lazy dog.
1183            ˇ
1184            ˇ
1185            ˇ
1186            ˇThe quick brown fox jumpsˇ
1187            ˇover the lazy dog.ˇ
1188            ˇ
1189            ˇ
1190            ˇ
1191        "},
1192        // "Blank" paragraph with whitespace characters
1193        indoc! {"
1194            ˇThe quick brown fox jumps
1195            over the lazy dog.
1196
1197            ˇ \t
1198
1199            ˇThe quick brown fox jumps
1200            over the lazy dog.ˇ
1201            ˇ
1202            ˇ \t
1203            \t \t
1204        "},
1205        // Single line "paragraphs", where selection size might be zero.
1206        indoc! {"
1207            ˇThe quick brown fox jumps over the lazy dog.
1208            ˇ
1209            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1210            ˇ
1211        "},
1212    ];
1213
1214    #[gpui::test]
1215    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1216        let mut cx = NeovimBackedTestContext::new(cx).await;
1217
1218        for paragraph_example in PARAGRAPH_EXAMPLES {
1219            cx.assert_binding_matches_all(["c", "i", "p"], paragraph_example)
1220                .await;
1221            cx.assert_binding_matches_all(["c", "a", "p"], paragraph_example)
1222                .await;
1223        }
1224    }
1225
1226    #[gpui::test]
1227    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1228        let mut cx = NeovimBackedTestContext::new(cx).await;
1229
1230        for paragraph_example in PARAGRAPH_EXAMPLES {
1231            cx.assert_binding_matches_all(["d", "i", "p"], paragraph_example)
1232                .await;
1233            cx.assert_binding_matches_all(["d", "a", "p"], paragraph_example)
1234                .await;
1235        }
1236    }
1237
1238    #[gpui::test]
1239    async fn test_paragraph_object_with_landing_positions_not_at_beginning_of_line(
1240        cx: &mut gpui::TestAppContext,
1241    ) {
1242        // Landing position not at the beginning of the line
1243        const PARAGRAPH_LANDING_POSITION_EXAMPLE: &'static str = indoc! {"
1244            The quick brown fox jumpsˇ
1245            over the lazy dog.ˇ
1246            ˇ ˇ\tˇ
1247            ˇ ˇ
1248            ˇ\tˇ ˇ\tˇ
1249            ˇThe quick brown fox jumpsˇ
1250            ˇover the lazy dog.ˇ
1251            ˇ ˇ\tˇ
1252            ˇ
1253            ˇ ˇ\tˇ
1254            ˇ\tˇ ˇ\tˇ
1255        "};
1256
1257        let mut cx = NeovimBackedTestContext::new(cx).await;
1258
1259        cx.assert_binding_matches_all_exempted(
1260            ["c", "i", "p"],
1261            PARAGRAPH_LANDING_POSITION_EXAMPLE,
1262            ExemptionFeatures::IncorrectLandingPosition,
1263        )
1264        .await;
1265        cx.assert_binding_matches_all_exempted(
1266            ["c", "a", "p"],
1267            PARAGRAPH_LANDING_POSITION_EXAMPLE,
1268            ExemptionFeatures::IncorrectLandingPosition,
1269        )
1270        .await;
1271        cx.assert_binding_matches_all_exempted(
1272            ["d", "i", "p"],
1273            PARAGRAPH_LANDING_POSITION_EXAMPLE,
1274            ExemptionFeatures::IncorrectLandingPosition,
1275        )
1276        .await;
1277        cx.assert_binding_matches_all_exempted(
1278            ["d", "a", "p"],
1279            PARAGRAPH_LANDING_POSITION_EXAMPLE,
1280            ExemptionFeatures::IncorrectLandingPosition,
1281        )
1282        .await;
1283    }
1284
1285    #[gpui::test]
1286    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1287        let mut cx = NeovimBackedTestContext::new(cx).await;
1288
1289        const EXAMPLES: &[&'static str] = &[
1290            indoc! {"
1291                ˇThe quick brown
1292                fox jumps over
1293                the lazy dog.
1294            "},
1295            indoc! {"
1296                ˇ
1297
1298                ˇThe quick brown fox jumps
1299                over the lazy dog.
1300                ˇ
1301
1302                ˇThe quick brown fox jumps
1303                over the lazy dog.
1304            "},
1305            indoc! {"
1306                ˇThe quick brown fox jumps over the lazy dog.
1307                ˇ
1308                ˇThe quick brown fox jumps over the lazy dog.
1309
1310            "},
1311        ];
1312
1313        for paragraph_example in EXAMPLES {
1314            cx.assert_binding_matches_all(["v", "i", "p"], paragraph_example)
1315                .await;
1316            cx.assert_binding_matches_all(["v", "a", "p"], paragraph_example)
1317                .await;
1318        }
1319    }
1320
1321    // Test string with "`" for opening surrounders and "'" for closing surrounders
1322    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1323        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1324        'ˇfox juˇmps ovˇ`ˇer
1325        the ˇlazy dˇ'ˇoˇ`ˇg"};
1326
1327    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1328        ('\'', '\''), // Quote
1329        ('`', '`'),   // Back Quote
1330        ('"', '"'),   // Double Quote
1331        ('(', ')'),   // Parentheses
1332        ('[', ']'),   // SquareBrackets
1333        ('{', '}'),   // CurlyBrackets
1334        ('<', '>'),   // AngleBrackets
1335    ];
1336
1337    #[gpui::test]
1338    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1339        let mut cx = NeovimBackedTestContext::new(cx).await;
1340
1341        for (start, end) in SURROUNDING_OBJECTS {
1342            let marked_string = SURROUNDING_MARKER_STRING
1343                .replace('`', &start.to_string())
1344                .replace('\'', &end.to_string());
1345
1346            cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
1347                .await;
1348            cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
1349                .await;
1350            cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
1351                .await;
1352            cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
1353                .await;
1354        }
1355    }
1356    #[gpui::test]
1357    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1358        let mut cx = NeovimBackedTestContext::new(cx).await;
1359        cx.set_shared_wrap(12).await;
1360
1361        cx.set_shared_state(indoc! {
1362            "helˇlo \"world\"!"
1363        })
1364        .await;
1365        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1366        cx.assert_shared_state(indoc! {
1367            "hello \"«worldˇ»\"!"
1368        })
1369        .await;
1370
1371        cx.set_shared_state(indoc! {
1372            "hello \"wˇorld\"!"
1373        })
1374        .await;
1375        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
1376        cx.assert_shared_state(indoc! {
1377            "hello \"«worldˇ»\"!"
1378        })
1379        .await;
1380
1381        cx.set_shared_state(indoc! {
1382            "hello \"wˇorld\"!"
1383        })
1384        .await;
1385        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1386        cx.assert_shared_state(indoc! {
1387            "hello« \"world\"ˇ»!"
1388        })
1389        .await;
1390
1391        cx.set_shared_state(indoc! {
1392            "hello \"wˇorld\" !"
1393        })
1394        .await;
1395        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1396        cx.assert_shared_state(indoc! {
1397            "hello «\"world\" ˇ»!"
1398        })
1399        .await;
1400
1401        cx.set_shared_state(indoc! {
1402            "hello \"wˇorld\"1403            goodbye"
1404        })
1405        .await;
1406        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
1407        cx.assert_shared_state(indoc! {
1408            "hello «\"world\" ˇ»
1409            goodbye"
1410        })
1411        .await;
1412    }
1413
1414    #[gpui::test]
1415    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1416        let mut cx = NeovimBackedTestContext::new(cx).await;
1417
1418        cx.set_shared_state(indoc! {
1419            "func empty(a string) bool {
1420               if a == \"\" {
1421                  return true
1422               }
1423               ˇreturn false
1424            }"
1425        })
1426        .await;
1427        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1428        cx.assert_shared_state(indoc! {"
1429            func empty(a string) bool {
1430            «   if a == \"\" {
1431                  return true
1432               }
1433               return false
1434            ˇ»}"})
1435            .await;
1436        cx.set_shared_state(indoc! {
1437            "func empty(a string) bool {
1438                 if a == \"\" {
1439                     ˇreturn true
1440                 }
1441                 return false
1442            }"
1443        })
1444        .await;
1445        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1446        cx.assert_shared_state(indoc! {"
1447            func empty(a string) bool {
1448                 if a == \"\" {
1449            «         return true
1450            ˇ»     }
1451                 return false
1452            }"})
1453            .await;
1454
1455        cx.set_shared_state(indoc! {
1456            "func empty(a string) bool {
1457                 if a == \"\" ˇ{
1458                     return true
1459                 }
1460                 return false
1461            }"
1462        })
1463        .await;
1464        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
1465        cx.assert_shared_state(indoc! {"
1466            func empty(a string) bool {
1467                 if a == \"\" {
1468            «         return true
1469            ˇ»     }
1470                 return false
1471            }"})
1472            .await;
1473    }
1474
1475    #[gpui::test]
1476    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1477        let mut cx = VimTestContext::new(cx, true).await;
1478        cx.set_state(
1479            indoc! {"
1480            fn boop() {
1481                baz(ˇ|a, b| { bar(|j, k| { })})
1482            }"
1483            },
1484            Mode::Normal,
1485        );
1486        cx.simulate_keystrokes(["c", "i", "|"]);
1487        cx.assert_state(
1488            indoc! {"
1489            fn boop() {
1490                baz(|ˇ| { bar(|j, k| { })})
1491            }"
1492            },
1493            Mode::Insert,
1494        );
1495        cx.simulate_keystrokes(["escape", "1", "8", "|"]);
1496        cx.assert_state(
1497            indoc! {"
1498            fn boop() {
1499                baz(|| { bar(ˇ|j, k| { })})
1500            }"
1501            },
1502            Mode::Normal,
1503        );
1504
1505        cx.simulate_keystrokes(["v", "a", "|"]);
1506        cx.assert_state(
1507            indoc! {"
1508            fn boop() {
1509                baz(|| { bar(«|j, k| ˇ»{ })})
1510            }"
1511            },
1512            Mode::Visual,
1513        );
1514    }
1515
1516    #[gpui::test]
1517    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1518        let mut cx = VimTestContext::new(cx, true).await;
1519
1520        // Generic arguments
1521        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1522        cx.simulate_keystrokes(["v", "i", "a"]);
1523        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1524
1525        // Function arguments
1526        cx.set_state(
1527            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1528            Mode::Normal,
1529        );
1530        cx.simulate_keystrokes(["d", "a", "a"]);
1531        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1532
1533        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1534        cx.simulate_keystrokes(["v", "a", "a"]);
1535        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1536
1537        // Tuple, vec, and array arguments
1538        cx.set_state(
1539            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1540            Mode::Normal,
1541        );
1542        cx.simulate_keystrokes(["c", "i", "a"]);
1543        cx.assert_state(
1544            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1545            Mode::Insert,
1546        );
1547
1548        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1549        cx.simulate_keystrokes(["c", "a", "a"]);
1550        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1551
1552        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1553        cx.simulate_keystrokes(["c", "i", "a"]);
1554        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1555
1556        cx.set_state(
1557            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1558            Mode::Normal,
1559        );
1560        cx.simulate_keystrokes(["c", "a", "a"]);
1561        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1562
1563        // Cursor immediately before / after brackets
1564        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1565        cx.simulate_keystrokes(["v", "i", "a"]);
1566        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1567
1568        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1569        cx.simulate_keystrokes(["v", "i", "a"]);
1570        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1571    }
1572
1573    #[gpui::test]
1574    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1575        let mut cx = NeovimBackedTestContext::new(cx).await;
1576
1577        for (start, end) in SURROUNDING_OBJECTS {
1578            let marked_string = SURROUNDING_MARKER_STRING
1579                .replace('`', &start.to_string())
1580                .replace('\'', &end.to_string());
1581
1582            cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
1583                .await;
1584            cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
1585                .await;
1586            cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
1587                .await;
1588            cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
1589                .await;
1590        }
1591    }
1592
1593    #[gpui::test]
1594    async fn test_tags(cx: &mut gpui::TestAppContext) {
1595        let mut cx = VimTestContext::new_html(cx).await;
1596
1597        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1598        cx.simulate_keystrokes(["v", "i", "t"]);
1599        cx.assert_state(
1600            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1601            Mode::Visual,
1602        );
1603        cx.simulate_keystrokes(["a", "t"]);
1604        cx.assert_state(
1605            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1606            Mode::Visual,
1607        );
1608        cx.simulate_keystrokes(["a", "t"]);
1609        cx.assert_state(
1610            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1611            Mode::Visual,
1612        );
1613
1614        // The cursor is before the tag
1615        cx.set_state(
1616            "<html><head></head><body> ˇ  <b>hi!</b></body>",
1617            Mode::Normal,
1618        );
1619        cx.simulate_keystrokes(["v", "i", "t"]);
1620        cx.assert_state(
1621            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
1622            Mode::Visual,
1623        );
1624        cx.simulate_keystrokes(["a", "t"]);
1625        cx.assert_state(
1626            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
1627            Mode::Visual,
1628        );
1629
1630        // The cursor is in the open tag
1631        cx.set_state(
1632            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1633            Mode::Normal,
1634        );
1635        cx.simulate_keystrokes(["v", "a", "t"]);
1636        cx.assert_state(
1637            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1638            Mode::Visual,
1639        );
1640        cx.simulate_keystrokes(["i", "t"]);
1641        cx.assert_state(
1642            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1643            Mode::Visual,
1644        );
1645
1646        // current selection length greater than 1
1647        cx.set_state(
1648            "<html><head></head><body><«b>hi!ˇ»</b></body>",
1649            Mode::Visual,
1650        );
1651        cx.simulate_keystrokes(["i", "t"]);
1652        cx.assert_state(
1653            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1654            Mode::Visual,
1655        );
1656        cx.simulate_keystrokes(["a", "t"]);
1657        cx.assert_state(
1658            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1659            Mode::Visual,
1660        );
1661
1662        cx.set_state(
1663            "<html><head></head><body><«b>hi!</ˇ»b></body>",
1664            Mode::Visual,
1665        );
1666        cx.simulate_keystrokes(["a", "t"]);
1667        cx.assert_state(
1668            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1669            Mode::Visual,
1670        );
1671    }
1672}