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