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