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