object.rs

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