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    let offset = excerpt.map_offset_to_buffer(offset);
 518
 519    let mut matches: Vec<Range<usize>> = buffer
 520        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 521        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 522        .collect();
 523    matches.sort_by_key(|r| (r.end - r.start));
 524    if let Some(buffer_range) = matches.first() {
 525        let range = excerpt.map_range_from_buffer(buffer_range.clone());
 526        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 527    }
 528
 529    let around = target.around()?;
 530    let mut matches: Vec<Range<usize>> = buffer
 531        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 532        .filter_map(|(r, m)| if m == around { Some(r) } else { None })
 533        .collect();
 534    matches.sort_by_key(|r| (r.end - r.start));
 535    let around_range = matches.first()?;
 536
 537    let mut matches: Vec<Range<usize>> = buffer
 538        .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
 539        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 540        .collect();
 541    matches.sort_by_key(|r| r.start);
 542    if let Some(buffer_range) = matches.first() {
 543        if !buffer_range.is_empty() {
 544            let range = excerpt.map_range_from_buffer(buffer_range.clone());
 545            return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 546        }
 547    }
 548    let buffer_range = excerpt.map_range_from_buffer(around_range.clone());
 549    return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
 550}
 551
 552fn argument(
 553    map: &DisplaySnapshot,
 554    relative_to: DisplayPoint,
 555    around: bool,
 556) -> Option<Range<DisplayPoint>> {
 557    let snapshot = &map.buffer_snapshot;
 558    let offset = relative_to.to_offset(map, Bias::Left);
 559
 560    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 561    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 562    let buffer = excerpt.buffer();
 563
 564    fn comma_delimited_range_at(
 565        buffer: &BufferSnapshot,
 566        mut offset: usize,
 567        include_comma: bool,
 568    ) -> Option<Range<usize>> {
 569        // Seek to the first non-whitespace character
 570        offset += buffer
 571            .chars_at(offset)
 572            .take_while(|c| c.is_whitespace())
 573            .map(char::len_utf8)
 574            .sum::<usize>();
 575
 576        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 577            // Filter out empty ranges
 578            if open.end == close.start {
 579                return false;
 580            }
 581
 582            // If the cursor is outside the brackets, ignore them
 583            if open.start == offset || close.end == offset {
 584                return false;
 585            }
 586
 587            // TODO: Is there any better way to filter out string brackets?
 588            // Used to filter out string brackets
 589            matches!(
 590                buffer.chars_at(open.start).next(),
 591                Some('(' | '[' | '{' | '<' | '|')
 592            )
 593        };
 594
 595        // Find the brackets containing the cursor
 596        let (open_bracket, close_bracket) =
 597            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 598
 599        let inner_bracket_range = open_bracket.end..close_bracket.start;
 600
 601        let layer = buffer.syntax_layer_at(offset)?;
 602        let node = layer.node();
 603        let mut cursor = node.walk();
 604
 605        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 606        let mut parent_covers_bracket_range = false;
 607        loop {
 608            let node = cursor.node();
 609            let range = node.byte_range();
 610            let covers_bracket_range =
 611                range.start == open_bracket.start && range.end == close_bracket.end;
 612            if parent_covers_bracket_range && !covers_bracket_range {
 613                break;
 614            }
 615            parent_covers_bracket_range = covers_bracket_range;
 616
 617            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 618            cursor.goto_first_child_for_byte(offset)?;
 619        }
 620
 621        let mut argument_node = cursor.node();
 622
 623        // If the child node is the open bracket, move to the next sibling.
 624        if argument_node.byte_range() == open_bracket {
 625            if !cursor.goto_next_sibling() {
 626                return Some(inner_bracket_range);
 627            }
 628            argument_node = cursor.node();
 629        }
 630        // While the child node is the close bracket or a comma, move to the previous sibling
 631        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 632            if !cursor.goto_previous_sibling() {
 633                return Some(inner_bracket_range);
 634            }
 635            argument_node = cursor.node();
 636            if argument_node.byte_range() == open_bracket {
 637                return Some(inner_bracket_range);
 638            }
 639        }
 640
 641        // The start and end of the argument range, defaulting to the start and end of the argument node
 642        let mut start = argument_node.start_byte();
 643        let mut end = argument_node.end_byte();
 644
 645        let mut needs_surrounding_comma = include_comma;
 646
 647        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 648        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 649        while cursor.goto_previous_sibling() {
 650            let prev = cursor.node();
 651
 652            if prev.start_byte() < open_bracket.end {
 653                start = open_bracket.end;
 654                break;
 655            } else if prev.kind() == "," {
 656                if needs_surrounding_comma {
 657                    start = prev.start_byte();
 658                    needs_surrounding_comma = false;
 659                }
 660                break;
 661            } else if prev.start_byte() < start {
 662                start = prev.start_byte();
 663            }
 664        }
 665
 666        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 667        while cursor.goto_next_sibling() {
 668            let next = cursor.node();
 669
 670            if next.end_byte() > close_bracket.start {
 671                end = close_bracket.start;
 672                break;
 673            } else if next.kind() == "," {
 674                if needs_surrounding_comma {
 675                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 676                    if let Some(next_arg) = next.next_sibling() {
 677                        end = next_arg.start_byte();
 678                    } else {
 679                        end = next.end_byte();
 680                    }
 681                }
 682                break;
 683            } else if next.end_byte() > end {
 684                end = next.end_byte();
 685            }
 686        }
 687
 688        Some(start..end)
 689    }
 690
 691    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 692
 693    if excerpt.contains_buffer_range(result.clone()) {
 694        let result = excerpt.map_range_from_buffer(result);
 695        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 696    } else {
 697        None
 698    }
 699}
 700
 701fn indent(
 702    map: &DisplaySnapshot,
 703    relative_to: DisplayPoint,
 704    around: bool,
 705    include_below: bool,
 706) -> Option<Range<DisplayPoint>> {
 707    let point = relative_to.to_point(map);
 708    let row = point.row;
 709
 710    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
 711
 712    // Loop backwards until we find a non-blank line with less indent
 713    let mut start_row = row;
 714    for prev_row in (0..row).rev() {
 715        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
 716        if indent.is_line_empty() {
 717            continue;
 718        }
 719        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 720            if around {
 721                // When around is true, include the first line with less indent
 722                start_row = prev_row;
 723            }
 724            break;
 725        }
 726        start_row = prev_row;
 727    }
 728
 729    // Loop forwards until we find a non-blank line with less indent
 730    let mut end_row = row;
 731    let max_rows = map.buffer_snapshot.max_row().0;
 732    for next_row in (row + 1)..=max_rows {
 733        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
 734        if indent.is_line_empty() {
 735            continue;
 736        }
 737        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 738            if around && include_below {
 739                // When around is true and including below, include this line
 740                end_row = next_row;
 741            }
 742            break;
 743        }
 744        end_row = next_row;
 745    }
 746
 747    let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
 748    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
 749    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
 750    Some(start..end)
 751}
 752
 753fn sentence(
 754    map: &DisplaySnapshot,
 755    relative_to: DisplayPoint,
 756    around: bool,
 757) -> Option<Range<DisplayPoint>> {
 758    let mut start = None;
 759    let relative_offset = relative_to.to_offset(map, Bias::Left);
 760    let mut previous_end = relative_offset;
 761
 762    let mut chars = map.buffer_chars_at(previous_end).peekable();
 763
 764    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 765    for (char, offset) in chars
 766        .peek()
 767        .cloned()
 768        .into_iter()
 769        .chain(map.reverse_buffer_chars_at(previous_end))
 770    {
 771        if is_sentence_end(map, offset) {
 772            break;
 773        }
 774
 775        if is_possible_sentence_start(char) {
 776            start = Some(offset);
 777        }
 778
 779        previous_end = offset;
 780    }
 781
 782    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 783    let mut end = relative_offset;
 784    for (char, offset) in chars {
 785        if start.is_none() && is_possible_sentence_start(char) {
 786            if around {
 787                start = Some(offset);
 788                continue;
 789            } else {
 790                end = offset;
 791                break;
 792            }
 793        }
 794
 795        if char != '\n' {
 796            end = offset + char.len_utf8();
 797        }
 798
 799        if is_sentence_end(map, end) {
 800            break;
 801        }
 802    }
 803
 804    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
 805    if around {
 806        range = expand_to_include_whitespace(map, range, false);
 807    }
 808
 809    Some(range)
 810}
 811
 812fn is_possible_sentence_start(character: char) -> bool {
 813    !character.is_whitespace() && character != '.'
 814}
 815
 816const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 817const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 818const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 819fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
 820    let mut next_chars = map.buffer_chars_at(offset).peekable();
 821    if let Some((char, _)) = next_chars.next() {
 822        // We are at a double newline. This position is a sentence end.
 823        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 824            return true;
 825        }
 826
 827        // The next text is not a valid whitespace. This is not a sentence end
 828        if !SENTENCE_END_WHITESPACE.contains(&char) {
 829            return false;
 830        }
 831    }
 832
 833    for (char, _) in map.reverse_buffer_chars_at(offset) {
 834        if SENTENCE_END_PUNCTUATION.contains(&char) {
 835            return true;
 836        }
 837
 838        if !SENTENCE_END_FILLERS.contains(&char) {
 839            return false;
 840        }
 841    }
 842
 843    false
 844}
 845
 846/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 847/// whitespace to the end first and falls back to the start if there was none.
 848fn expand_to_include_whitespace(
 849    map: &DisplaySnapshot,
 850    range: Range<DisplayPoint>,
 851    stop_at_newline: bool,
 852) -> Range<DisplayPoint> {
 853    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
 854    let mut whitespace_included = false;
 855
 856    let chars = map.buffer_chars_at(range.end).peekable();
 857    for (char, offset) in chars {
 858        if char == '\n' && stop_at_newline {
 859            break;
 860        }
 861
 862        if char.is_whitespace() {
 863            if char != '\n' {
 864                range.end = offset + char.len_utf8();
 865                whitespace_included = true;
 866            }
 867        } else {
 868            // Found non whitespace. Quit out.
 869            break;
 870        }
 871    }
 872
 873    if !whitespace_included {
 874        for (char, point) in map.reverse_buffer_chars_at(range.start) {
 875            if char == '\n' && stop_at_newline {
 876                break;
 877            }
 878
 879            if !char.is_whitespace() {
 880                break;
 881            }
 882
 883            range.start = point;
 884        }
 885    }
 886
 887    range.start.to_display_point(map)..range.end.to_display_point(map)
 888}
 889
 890/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
 891/// where `relative_to` is in. If `around`, principally returns the range ending
 892/// at the end of the next paragraph.
 893///
 894/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
 895/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
 896/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
 897/// the trailing newline is not subject to subsequent operations).
 898///
 899/// Edge cases:
 900/// - If `around` and if the current paragraph is the last paragraph of the
 901///   file and is blank, then the selection results in an error.
 902/// - If `around` and if the current paragraph is the last paragraph of the
 903///   file and is not blank, then the returned range starts at the start of the
 904///   previous paragraph, if it exists.
 905fn paragraph(
 906    map: &DisplaySnapshot,
 907    relative_to: DisplayPoint,
 908    around: bool,
 909) -> Option<Range<DisplayPoint>> {
 910    let mut paragraph_start = start_of_paragraph(map, relative_to);
 911    let mut paragraph_end = end_of_paragraph(map, relative_to);
 912
 913    let paragraph_end_row = paragraph_end.row();
 914    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
 915    let point = relative_to.to_point(map);
 916    let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 917
 918    if around {
 919        if paragraph_ends_with_eof {
 920            if current_line_is_empty {
 921                return None;
 922            }
 923
 924            let paragraph_start_row = paragraph_start.row();
 925            if paragraph_start_row.0 != 0 {
 926                let previous_paragraph_last_line_start =
 927                    DisplayPoint::new(paragraph_start_row - 1, 0);
 928                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
 929            }
 930        } else {
 931            let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
 932            paragraph_end = end_of_paragraph(map, next_paragraph_start);
 933        }
 934    }
 935
 936    let range = paragraph_start..paragraph_end;
 937    Some(range)
 938}
 939
 940/// Returns a position of the start of the current paragraph, where a paragraph
 941/// is defined as a run of non-blank lines or a run of blank lines.
 942pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 943    let point = display_point.to_point(map);
 944    if point.row == 0 {
 945        return DisplayPoint::zero();
 946    }
 947
 948    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 949
 950    for row in (0..point.row).rev() {
 951        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 952        if blank != is_current_line_blank {
 953            return Point::new(row + 1, 0).to_display_point(map);
 954        }
 955    }
 956
 957    DisplayPoint::zero()
 958}
 959
 960/// Returns a position of the end of the current paragraph, where a paragraph
 961/// is defined as a run of non-blank lines or a run of blank lines.
 962/// The trailing newline is excluded from the paragraph.
 963pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 964    let point = display_point.to_point(map);
 965    if point.row == map.buffer_snapshot.max_row().0 {
 966        return map.max_point();
 967    }
 968
 969    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 970
 971    for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
 972        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 973        if blank != is_current_line_blank {
 974            let previous_row = row - 1;
 975            return Point::new(
 976                previous_row,
 977                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
 978            )
 979            .to_display_point(map);
 980        }
 981    }
 982
 983    map.max_point()
 984}
 985
 986fn surrounding_markers(
 987    map: &DisplaySnapshot,
 988    relative_to: DisplayPoint,
 989    around: bool,
 990    search_across_lines: bool,
 991    open_marker: char,
 992    close_marker: char,
 993) -> Option<Range<DisplayPoint>> {
 994    let point = relative_to.to_offset(map, Bias::Left);
 995
 996    let mut matched_closes = 0;
 997    let mut opening = None;
 998
 999    let mut before_ch = match movement::chars_before(map, point).next() {
1000        Some((ch, _)) => ch,
1001        _ => '\0',
1002    };
1003    if let Some((ch, range)) = movement::chars_after(map, point).next() {
1004        if ch == open_marker && before_ch != '\\' {
1005            if open_marker == close_marker {
1006                let mut total = 0;
1007                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1008                {
1009                    if ch == '\n' {
1010                        break;
1011                    }
1012                    if ch == open_marker && before_ch != '\\' {
1013                        total += 1;
1014                    }
1015                }
1016                if total % 2 == 0 {
1017                    opening = Some(range)
1018                }
1019            } else {
1020                opening = Some(range)
1021            }
1022        }
1023    }
1024
1025    if opening.is_none() {
1026        let mut chars_before = movement::chars_before(map, point).peekable();
1027        while let Some((ch, range)) = chars_before.next() {
1028            if ch == '\n' && !search_across_lines {
1029                break;
1030            }
1031
1032            if let Some((before_ch, _)) = chars_before.peek() {
1033                if *before_ch == '\\' {
1034                    continue;
1035                }
1036            }
1037
1038            if ch == open_marker {
1039                if matched_closes == 0 {
1040                    opening = Some(range);
1041                    break;
1042                }
1043                matched_closes -= 1;
1044            } else if ch == close_marker {
1045                matched_closes += 1
1046            }
1047        }
1048    }
1049    if opening.is_none() {
1050        for (ch, range) in movement::chars_after(map, point) {
1051            if before_ch != '\\' {
1052                if ch == open_marker {
1053                    opening = Some(range);
1054                    break;
1055                } else if ch == close_marker {
1056                    break;
1057                }
1058            }
1059
1060            before_ch = ch;
1061        }
1062    }
1063
1064    let mut opening = opening?;
1065
1066    let mut matched_opens = 0;
1067    let mut closing = None;
1068    before_ch = match movement::chars_before(map, opening.end).next() {
1069        Some((ch, _)) => ch,
1070        _ => '\0',
1071    };
1072    for (ch, range) in movement::chars_after(map, opening.end) {
1073        if ch == '\n' && !search_across_lines {
1074            break;
1075        }
1076
1077        if before_ch != '\\' {
1078            if ch == close_marker {
1079                if matched_opens == 0 {
1080                    closing = Some(range);
1081                    break;
1082                }
1083                matched_opens -= 1;
1084            } else if ch == open_marker {
1085                matched_opens += 1;
1086            }
1087        }
1088
1089        before_ch = ch;
1090    }
1091
1092    let mut closing = closing?;
1093
1094    if around && !search_across_lines {
1095        let mut found = false;
1096
1097        for (ch, range) in movement::chars_after(map, closing.end) {
1098            if ch.is_whitespace() && ch != '\n' {
1099                found = true;
1100                closing.end = range.end;
1101            } else {
1102                break;
1103            }
1104        }
1105
1106        if !found {
1107            for (ch, range) in movement::chars_before(map, opening.start) {
1108                if ch.is_whitespace() && ch != '\n' {
1109                    opening.start = range.start
1110                } else {
1111                    break;
1112                }
1113            }
1114        }
1115    }
1116
1117    if !around && search_across_lines {
1118        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1119            if ch == '\n' {
1120                opening.end = range.end
1121            }
1122        }
1123
1124        for (ch, range) in movement::chars_before(map, closing.start) {
1125            if !ch.is_whitespace() {
1126                break;
1127            }
1128            if ch != '\n' {
1129                closing.start = range.start
1130            }
1131        }
1132    }
1133
1134    let result = if around {
1135        opening.start..closing.end
1136    } else {
1137        opening.end..closing.start
1138    };
1139
1140    Some(
1141        map.clip_point(result.start.to_display_point(map), Bias::Left)
1142            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1143    )
1144}
1145
1146#[cfg(test)]
1147mod test {
1148    use indoc::indoc;
1149
1150    use crate::{
1151        state::Mode,
1152        test::{NeovimBackedTestContext, VimTestContext},
1153    };
1154
1155    const WORD_LOCATIONS: &str = indoc! {"
1156        The quick ˇbrowˇnˇ•••
1157        fox ˇjuˇmpsˇ over
1158        the lazy dogˇ••
1159        ˇ
1160        ˇ
1161        ˇ
1162        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1163        ˇ••
1164        ˇ••
1165        ˇ  fox-jumpˇs over
1166        the lazy dogˇ•
1167        ˇ
1168        "
1169    };
1170
1171    #[gpui::test]
1172    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1173        let mut cx = NeovimBackedTestContext::new(cx).await;
1174
1175        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1176            .await
1177            .assert_matches();
1178        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1179            .await
1180            .assert_matches();
1181        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1182            .await
1183            .assert_matches();
1184        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1185            .await
1186            .assert_matches();
1187    }
1188
1189    #[gpui::test]
1190    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1191        let mut cx = NeovimBackedTestContext::new(cx).await;
1192
1193        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1194            .await
1195            .assert_matches();
1196        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1197            .await
1198            .assert_matches();
1199        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1200            .await
1201            .assert_matches();
1202        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1203            .await
1204            .assert_matches();
1205    }
1206
1207    #[gpui::test]
1208    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1209        let mut cx = NeovimBackedTestContext::new(cx).await;
1210
1211        /*
1212                cx.set_shared_state("The quick ˇbrown\nfox").await;
1213                cx.simulate_shared_keystrokes(["v"]).await;
1214                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1215                cx.simulate_shared_keystrokes(["i", "w"]).await;
1216                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1217        */
1218        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1219        cx.simulate_shared_keystrokes("v").await;
1220        cx.shared_state()
1221            .await
1222            .assert_eq("The quick brown\n«\nˇ»fox");
1223        cx.simulate_shared_keystrokes("i w").await;
1224        cx.shared_state()
1225            .await
1226            .assert_eq("The quick brown\n«\nˇ»fox");
1227
1228        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1229            .await
1230            .assert_matches();
1231        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1232            .await
1233            .assert_matches();
1234    }
1235
1236    const PARAGRAPH_EXAMPLES: &[&str] = &[
1237        // Single line
1238        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1239        // Multiple lines without empty lines
1240        indoc! {"
1241            ˇThe quick brownˇ
1242            ˇfox jumps overˇ
1243            the lazy dog.ˇ
1244        "},
1245        // Heading blank paragraph and trailing normal paragraph
1246        indoc! {"
1247            ˇ
1248            ˇ
1249            ˇThe quick brown fox jumps
1250            ˇover the lazy dog.
1251            ˇ
1252            ˇ
1253            ˇThe quick brown fox jumpsˇ
1254            ˇover the lazy dog.ˇ
1255        "},
1256        // Inserted blank paragraph and trailing blank paragraph
1257        indoc! {"
1258            ˇThe quick brown fox jumps
1259            ˇover the lazy dog.
1260            ˇ
1261            ˇ
1262            ˇ
1263            ˇThe quick brown fox jumpsˇ
1264            ˇover the lazy dog.ˇ
1265            ˇ
1266            ˇ
1267            ˇ
1268        "},
1269        // "Blank" paragraph with whitespace characters
1270        indoc! {"
1271            ˇThe quick brown fox jumps
1272            over the lazy dog.
1273
1274            ˇ \t
1275
1276            ˇThe quick brown fox jumps
1277            over the lazy dog.ˇ
1278            ˇ
1279            ˇ \t
1280            \t \t
1281        "},
1282        // Single line "paragraphs", where selection size might be zero.
1283        indoc! {"
1284            ˇThe quick brown fox jumps over the lazy dog.
1285            ˇ
1286            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1287            ˇ
1288        "},
1289    ];
1290
1291    #[gpui::test]
1292    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1293        let mut cx = NeovimBackedTestContext::new(cx).await;
1294
1295        for paragraph_example in PARAGRAPH_EXAMPLES {
1296            cx.simulate_at_each_offset("c i p", paragraph_example)
1297                .await
1298                .assert_matches();
1299            cx.simulate_at_each_offset("c a p", paragraph_example)
1300                .await
1301                .assert_matches();
1302        }
1303    }
1304
1305    #[gpui::test]
1306    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1307        let mut cx = NeovimBackedTestContext::new(cx).await;
1308
1309        for paragraph_example in PARAGRAPH_EXAMPLES {
1310            cx.simulate_at_each_offset("d i p", paragraph_example)
1311                .await
1312                .assert_matches();
1313            cx.simulate_at_each_offset("d a p", paragraph_example)
1314                .await
1315                .assert_matches();
1316        }
1317    }
1318
1319    #[gpui::test]
1320    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1321        let mut cx = NeovimBackedTestContext::new(cx).await;
1322
1323        const EXAMPLES: &[&str] = &[
1324            indoc! {"
1325                ˇThe quick brown
1326                fox jumps over
1327                the lazy dog.
1328            "},
1329            indoc! {"
1330                ˇ
1331
1332                ˇThe quick brown fox jumps
1333                over the lazy dog.
1334                ˇ
1335
1336                ˇThe quick brown fox jumps
1337                over the lazy dog.
1338            "},
1339            indoc! {"
1340                ˇThe quick brown fox jumps over the lazy dog.
1341                ˇ
1342                ˇThe quick brown fox jumps over the lazy dog.
1343
1344            "},
1345        ];
1346
1347        for paragraph_example in EXAMPLES {
1348            cx.simulate_at_each_offset("v i p", paragraph_example)
1349                .await
1350                .assert_matches();
1351            cx.simulate_at_each_offset("v a p", paragraph_example)
1352                .await
1353                .assert_matches();
1354        }
1355    }
1356
1357    // Test string with "`" for opening surrounders and "'" for closing surrounders
1358    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1359        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1360        'ˇfox juˇmps ov`ˇer
1361        the ˇlazy d'o`ˇg"};
1362
1363    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1364        ('"', '"'), // Double Quote
1365        ('(', ')'), // Parentheses
1366    ];
1367
1368    #[gpui::test]
1369    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1370        let mut cx = NeovimBackedTestContext::new(cx).await;
1371
1372        for (start, end) in SURROUNDING_OBJECTS {
1373            let marked_string = SURROUNDING_MARKER_STRING
1374                .replace('`', &start.to_string())
1375                .replace('\'', &end.to_string());
1376
1377            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1378                .await
1379                .assert_matches();
1380            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1381                .await
1382                .assert_matches();
1383            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1384                .await
1385                .assert_matches();
1386            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1387                .await
1388                .assert_matches();
1389        }
1390    }
1391    #[gpui::test]
1392    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1393        let mut cx = NeovimBackedTestContext::new(cx).await;
1394        cx.set_shared_wrap(12).await;
1395
1396        cx.set_shared_state(indoc! {
1397            "\"ˇhello world\"!"
1398        })
1399        .await;
1400        cx.simulate_shared_keystrokes("v i \"").await;
1401        cx.shared_state().await.assert_eq(indoc! {
1402            "\"«hello worldˇ»\"!"
1403        });
1404
1405        cx.set_shared_state(indoc! {
1406            "\"hˇello world\"!"
1407        })
1408        .await;
1409        cx.simulate_shared_keystrokes("v i \"").await;
1410        cx.shared_state().await.assert_eq(indoc! {
1411            "\"«hello worldˇ»\"!"
1412        });
1413
1414        cx.set_shared_state(indoc! {
1415            "helˇlo \"world\"!"
1416        })
1417        .await;
1418        cx.simulate_shared_keystrokes("v i \"").await;
1419        cx.shared_state().await.assert_eq(indoc! {
1420            "hello \"«worldˇ»\"!"
1421        });
1422
1423        cx.set_shared_state(indoc! {
1424            "hello \"wˇorld\"!"
1425        })
1426        .await;
1427        cx.simulate_shared_keystrokes("v i \"").await;
1428        cx.shared_state().await.assert_eq(indoc! {
1429            "hello \"«worldˇ»\"!"
1430        });
1431
1432        cx.set_shared_state(indoc! {
1433            "hello \"wˇorld\"!"
1434        })
1435        .await;
1436        cx.simulate_shared_keystrokes("v a \"").await;
1437        cx.shared_state().await.assert_eq(indoc! {
1438            "hello« \"world\"ˇ»!"
1439        });
1440
1441        cx.set_shared_state(indoc! {
1442            "hello \"wˇorld\" !"
1443        })
1444        .await;
1445        cx.simulate_shared_keystrokes("v a \"").await;
1446        cx.shared_state().await.assert_eq(indoc! {
1447            "hello «\"world\" ˇ»!"
1448        });
1449
1450        cx.set_shared_state(indoc! {
1451            "hello \"wˇorld\"1452            goodbye"
1453        })
1454        .await;
1455        cx.simulate_shared_keystrokes("v a \"").await;
1456        cx.shared_state().await.assert_eq(indoc! {
1457            "hello «\"world\" ˇ»
1458            goodbye"
1459        });
1460    }
1461
1462    #[gpui::test]
1463    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1464        let mut cx = NeovimBackedTestContext::new(cx).await;
1465
1466        cx.set_shared_state(indoc! {
1467            "func empty(a string) bool {
1468               if a == \"\" {
1469                  return true
1470               }
1471               ˇreturn false
1472            }"
1473        })
1474        .await;
1475        cx.simulate_shared_keystrokes("v i {").await;
1476        cx.shared_state().await.assert_eq(indoc! {"
1477            func empty(a string) bool {
1478            «   if a == \"\" {
1479                  return true
1480               }
1481               return false
1482            ˇ»}"});
1483        cx.set_shared_state(indoc! {
1484            "func empty(a string) bool {
1485                 if a == \"\" {
1486                     ˇreturn true
1487                 }
1488                 return false
1489            }"
1490        })
1491        .await;
1492        cx.simulate_shared_keystrokes("v i {").await;
1493        cx.shared_state().await.assert_eq(indoc! {"
1494            func empty(a string) bool {
1495                 if a == \"\" {
1496            «         return true
1497            ˇ»     }
1498                 return false
1499            }"});
1500
1501        cx.set_shared_state(indoc! {
1502            "func empty(a string) bool {
1503                 if a == \"\" ˇ{
1504                     return true
1505                 }
1506                 return false
1507            }"
1508        })
1509        .await;
1510        cx.simulate_shared_keystrokes("v i {").await;
1511        cx.shared_state().await.assert_eq(indoc! {"
1512            func empty(a string) bool {
1513                 if a == \"\" {
1514            «         return true
1515            ˇ»     }
1516                 return false
1517            }"});
1518    }
1519
1520    #[gpui::test]
1521    async fn test_singleline_surrounding_character_objects_with_escape(
1522        cx: &mut gpui::TestAppContext,
1523    ) {
1524        let mut cx = NeovimBackedTestContext::new(cx).await;
1525        cx.set_shared_state(indoc! {
1526            "h\"e\\\"lˇlo \\\"world\"!"
1527        })
1528        .await;
1529        cx.simulate_shared_keystrokes("v i \"").await;
1530        cx.shared_state().await.assert_eq(indoc! {
1531            "h\"«e\\\"llo \\\"worldˇ»\"!"
1532        });
1533
1534        cx.set_shared_state(indoc! {
1535            "hello \"teˇst \\\"inside\\\" world\""
1536        })
1537        .await;
1538        cx.simulate_shared_keystrokes("v i \"").await;
1539        cx.shared_state().await.assert_eq(indoc! {
1540            "hello \"«test \\\"inside\\\" worldˇ»\""
1541        });
1542    }
1543
1544    #[gpui::test]
1545    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1546        let mut cx = VimTestContext::new(cx, true).await;
1547        cx.set_state(
1548            indoc! {"
1549            fn boop() {
1550                baz(ˇ|a, b| { bar(|j, k| { })})
1551            }"
1552            },
1553            Mode::Normal,
1554        );
1555        cx.simulate_keystrokes("c i |");
1556        cx.assert_state(
1557            indoc! {"
1558            fn boop() {
1559                baz(|ˇ| { bar(|j, k| { })})
1560            }"
1561            },
1562            Mode::Insert,
1563        );
1564        cx.simulate_keystrokes("escape 1 8 |");
1565        cx.assert_state(
1566            indoc! {"
1567            fn boop() {
1568                baz(|| { bar(ˇ|j, k| { })})
1569            }"
1570            },
1571            Mode::Normal,
1572        );
1573
1574        cx.simulate_keystrokes("v a |");
1575        cx.assert_state(
1576            indoc! {"
1577            fn boop() {
1578                baz(|| { bar(«|j, k| ˇ»{ })})
1579            }"
1580            },
1581            Mode::Visual,
1582        );
1583    }
1584
1585    #[gpui::test]
1586    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1587        let mut cx = VimTestContext::new(cx, true).await;
1588
1589        // Generic arguments
1590        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1591        cx.simulate_keystrokes("v i a");
1592        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1593
1594        // Function arguments
1595        cx.set_state(
1596            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1597            Mode::Normal,
1598        );
1599        cx.simulate_keystrokes("d a a");
1600        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1601
1602        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1603        cx.simulate_keystrokes("v a a");
1604        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1605
1606        // Tuple, vec, and array arguments
1607        cx.set_state(
1608            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1609            Mode::Normal,
1610        );
1611        cx.simulate_keystrokes("c i a");
1612        cx.assert_state(
1613            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1614            Mode::Insert,
1615        );
1616
1617        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1618        cx.simulate_keystrokes("c a a");
1619        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1620
1621        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1622        cx.simulate_keystrokes("c i a");
1623        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1624
1625        cx.set_state(
1626            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1627            Mode::Normal,
1628        );
1629        cx.simulate_keystrokes("c a a");
1630        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1631
1632        // Cursor immediately before / after brackets
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        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1638        cx.simulate_keystrokes("v i a");
1639        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1640    }
1641
1642    #[gpui::test]
1643    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1644        let mut cx = VimTestContext::new(cx, true).await;
1645
1646        // Base use case
1647        cx.set_state(
1648            indoc! {"
1649                fn boop() {
1650                    // Comment
1651                    baz();ˇ
1652
1653                    loop {
1654                        bar(1);
1655                        bar(2);
1656                    }
1657
1658                    result
1659                }
1660            "},
1661            Mode::Normal,
1662        );
1663        cx.simulate_keystrokes("v i i");
1664        cx.assert_state(
1665            indoc! {"
1666                fn boop() {
1667                «    // Comment
1668                    baz();
1669
1670                    loop {
1671                        bar(1);
1672                        bar(2);
1673                    }
1674
1675                    resultˇ»
1676                }
1677            "},
1678            Mode::Visual,
1679        );
1680
1681        // Around indent (include line above)
1682        cx.set_state(
1683            indoc! {"
1684                const ABOVE: str = true;
1685                fn boop() {
1686
1687                    hello();
1688                    worˇld()
1689                }
1690            "},
1691            Mode::Normal,
1692        );
1693        cx.simulate_keystrokes("v a i");
1694        cx.assert_state(
1695            indoc! {"
1696                const ABOVE: str = true;
1697                «fn boop() {
1698
1699                    hello();
1700                    world()ˇ»
1701                }
1702            "},
1703            Mode::Visual,
1704        );
1705
1706        // Around indent (include line above & below)
1707        cx.set_state(
1708            indoc! {"
1709                const ABOVE: str = true;
1710                fn boop() {
1711                    hellˇo();
1712                    world()
1713
1714                }
1715                const BELOW: str = true;
1716            "},
1717            Mode::Normal,
1718        );
1719        cx.simulate_keystrokes("c a shift-i");
1720        cx.assert_state(
1721            indoc! {"
1722                const ABOVE: str = true;
1723                ˇ
1724                const BELOW: str = true;
1725            "},
1726            Mode::Insert,
1727        );
1728    }
1729
1730    #[gpui::test]
1731    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1732        let mut cx = NeovimBackedTestContext::new(cx).await;
1733
1734        for (start, end) in SURROUNDING_OBJECTS {
1735            let marked_string = SURROUNDING_MARKER_STRING
1736                .replace('`', &start.to_string())
1737                .replace('\'', &end.to_string());
1738
1739            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1740                .await
1741                .assert_matches();
1742            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1743                .await
1744                .assert_matches();
1745            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1746                .await
1747                .assert_matches();
1748            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1749                .await
1750                .assert_matches();
1751        }
1752    }
1753
1754    #[gpui::test]
1755    async fn test_tags(cx: &mut gpui::TestAppContext) {
1756        let mut cx = VimTestContext::new_html(cx).await;
1757
1758        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1759        cx.simulate_keystrokes("v i t");
1760        cx.assert_state(
1761            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1762            Mode::Visual,
1763        );
1764        cx.simulate_keystrokes("a t");
1765        cx.assert_state(
1766            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1767            Mode::Visual,
1768        );
1769        cx.simulate_keystrokes("a t");
1770        cx.assert_state(
1771            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1772            Mode::Visual,
1773        );
1774
1775        // The cursor is before the tag
1776        cx.set_state(
1777            "<html><head></head><body> ˇ  <b>hi!</b></body>",
1778            Mode::Normal,
1779        );
1780        cx.simulate_keystrokes("v i t");
1781        cx.assert_state(
1782            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
1783            Mode::Visual,
1784        );
1785        cx.simulate_keystrokes("a t");
1786        cx.assert_state(
1787            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
1788            Mode::Visual,
1789        );
1790
1791        // The cursor is in the open tag
1792        cx.set_state(
1793            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1794            Mode::Normal,
1795        );
1796        cx.simulate_keystrokes("v a t");
1797        cx.assert_state(
1798            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1799            Mode::Visual,
1800        );
1801        cx.simulate_keystrokes("i t");
1802        cx.assert_state(
1803            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1804            Mode::Visual,
1805        );
1806
1807        // current selection length greater than 1
1808        cx.set_state(
1809            "<html><head></head><body><«b>hi!ˇ»</b></body>",
1810            Mode::Visual,
1811        );
1812        cx.simulate_keystrokes("i t");
1813        cx.assert_state(
1814            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1815            Mode::Visual,
1816        );
1817        cx.simulate_keystrokes("a t");
1818        cx.assert_state(
1819            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1820            Mode::Visual,
1821        );
1822
1823        cx.set_state(
1824            "<html><head></head><body><«b>hi!</ˇ»b></body>",
1825            Mode::Visual,
1826        );
1827        cx.simulate_keystrokes("a t");
1828        cx.assert_state(
1829            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1830            Mode::Visual,
1831        );
1832    }
1833}