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