object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    Vim,
   5    motion::{is_subword_end, is_subword_start, right},
   6    state::{Mode, Operator},
   7};
   8use editor::{
   9    Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
  10    display_map::{DisplaySnapshot, ToDisplayPoint},
  11    movement::{self, FindRange},
  12};
  13use gpui::{Action, Window, actions};
  14use itertools::Itertools;
  15use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
  16use multi_buffer::MultiBufferRow;
  17use schemars::JsonSchema;
  18use serde::Deserialize;
  19use ui::Context;
  20
  21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
  22#[serde(rename_all = "snake_case")]
  23pub enum Object {
  24    Word { ignore_punctuation: bool },
  25    Subword { ignore_punctuation: bool },
  26    Sentence,
  27    Paragraph,
  28    Quotes,
  29    BackQuotes,
  30    AnyQuotes,
  31    MiniQuotes,
  32    DoubleQuotes,
  33    VerticalBars,
  34    AnyBrackets,
  35    MiniBrackets,
  36    Parentheses,
  37    SquareBrackets,
  38    CurlyBrackets,
  39    AngleBrackets,
  40    Argument,
  41    IndentObj { include_below: bool },
  42    Tag,
  43    Method,
  44    Class,
  45    Comment,
  46    EntireFile,
  47}
  48
  49/// Selects a word text object.
  50#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  51#[action(namespace = vim)]
  52#[serde(deny_unknown_fields)]
  53struct Word {
  54    #[serde(default)]
  55    ignore_punctuation: bool,
  56}
  57
  58/// Selects a subword text object.
  59#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  60#[action(namespace = vim)]
  61#[serde(deny_unknown_fields)]
  62struct Subword {
  63    #[serde(default)]
  64    ignore_punctuation: bool,
  65}
  66/// Selects text at the same indentation level.
  67#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  68#[action(namespace = vim)]
  69#[serde(deny_unknown_fields)]
  70struct IndentObj {
  71    #[serde(default)]
  72    include_below: bool,
  73}
  74
  75#[derive(Debug, Clone)]
  76pub struct CandidateRange {
  77    pub start: DisplayPoint,
  78    pub end: DisplayPoint,
  79}
  80
  81#[derive(Debug, Clone)]
  82pub struct CandidateWithRanges {
  83    candidate: CandidateRange,
  84    open_range: Range<MultiBufferOffset>,
  85    close_range: Range<MultiBufferOffset>,
  86}
  87
  88/// Operates on text within or around parentheses `()`.
  89#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  90#[action(namespace = vim)]
  91#[serde(deny_unknown_fields)]
  92struct Parentheses {
  93    #[serde(default)]
  94    opening: bool,
  95}
  96
  97/// Operates on text within or around square brackets `[]`.
  98#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  99#[action(namespace = vim)]
 100#[serde(deny_unknown_fields)]
 101struct SquareBrackets {
 102    #[serde(default)]
 103    opening: bool,
 104}
 105
 106/// Operates on text within or around angle brackets `<>`.
 107#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 108#[action(namespace = vim)]
 109#[serde(deny_unknown_fields)]
 110struct AngleBrackets {
 111    #[serde(default)]
 112    opening: bool,
 113}
 114
 115/// Operates on text within or around curly brackets `{}`.
 116#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 117#[action(namespace = vim)]
 118#[serde(deny_unknown_fields)]
 119struct CurlyBrackets {
 120    #[serde(default)]
 121    opening: bool,
 122}
 123
 124fn cover_or_next<I: Iterator<Item = (Range<MultiBufferOffset>, Range<MultiBufferOffset>)>>(
 125    candidates: Option<I>,
 126    caret: DisplayPoint,
 127    map: &DisplaySnapshot,
 128) -> Option<CandidateWithRanges> {
 129    let caret_offset = caret.to_offset(map, Bias::Left);
 130    let mut covering = vec![];
 131    let mut next_ones = vec![];
 132    let snapshot = map.buffer_snapshot();
 133
 134    if let Some(ranges) = candidates {
 135        for (open_range, close_range) in ranges {
 136            let start_off = open_range.start;
 137            let end_off = close_range.end;
 138            let candidate = CandidateWithRanges {
 139                candidate: CandidateRange {
 140                    start: start_off.to_display_point(map),
 141                    end: end_off.to_display_point(map),
 142                },
 143                open_range: open_range.clone(),
 144                close_range: close_range.clone(),
 145            };
 146
 147            if open_range
 148                .start
 149                .to_offset(snapshot)
 150                .to_display_point(map)
 151                .row()
 152                == caret_offset.to_display_point(map).row()
 153            {
 154                if start_off <= caret_offset && caret_offset < end_off {
 155                    covering.push(candidate);
 156                } else if start_off >= caret_offset {
 157                    next_ones.push(candidate);
 158                }
 159            }
 160        }
 161    }
 162
 163    // 1) covering -> smallest width
 164    if !covering.is_empty() {
 165        return covering.into_iter().min_by_key(|r| {
 166            r.candidate.end.to_offset(map, Bias::Right)
 167                - r.candidate.start.to_offset(map, Bias::Left)
 168        });
 169    }
 170
 171    // 2) next -> closest by start
 172    if !next_ones.is_empty() {
 173        return next_ones.into_iter().min_by_key(|r| {
 174            let start = r.candidate.start.to_offset(map, Bias::Left);
 175            (start.0 as isize - caret_offset.0 as isize).abs()
 176        });
 177    }
 178
 179    None
 180}
 181
 182type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool;
 183
 184struct DelimiterRange {
 185    open: Range<MultiBufferOffset>,
 186    close: Range<MultiBufferOffset>,
 187}
 188
 189impl DelimiterRange {
 190    fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range<DisplayPoint> {
 191        if around {
 192            self.open.start.to_display_point(map)..self.close.end.to_display_point(map)
 193        } else {
 194            self.open.end.to_display_point(map)..self.close.start.to_display_point(map)
 195        }
 196    }
 197}
 198
 199fn find_mini_delimiters(
 200    map: &DisplaySnapshot,
 201    display_point: DisplayPoint,
 202    around: bool,
 203    is_valid_delimiter: &DelimiterPredicate,
 204) -> Option<Range<DisplayPoint>> {
 205    let point = map.clip_at_line_end(display_point).to_point(map);
 206    let offset = point.to_offset(&map.buffer_snapshot());
 207
 208    let line_range = get_line_range(map, point);
 209    let visible_line_range = get_visible_line_range(&line_range);
 210
 211    let snapshot = &map.buffer_snapshot();
 212    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 213    let buffer = excerpt.buffer();
 214    let buffer_offset = excerpt.map_offset_to_buffer(offset);
 215
 216    let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 217        is_valid_delimiter(buffer, open.start, close.start)
 218    };
 219
 220    // Try to find delimiters in visible range first
 221    let ranges = map
 222        .buffer_snapshot()
 223        .bracket_ranges(visible_line_range)
 224        .map(|ranges| {
 225            ranges.filter_map(|(open, close)| {
 226                // Convert the ranges from multibuffer space to buffer space as
 227                // that is what `is_valid_delimiter` expects, otherwise it might
 228                // panic as the values might be out of bounds.
 229                let buffer_open = excerpt.map_range_to_buffer(open.clone());
 230                let buffer_close = excerpt.map_range_to_buffer(close.clone());
 231
 232                if is_valid_delimiter(buffer, buffer_open.start.0, buffer_close.start.0) {
 233                    Some((open, close))
 234                } else {
 235                    None
 236                }
 237            })
 238        });
 239
 240    if let Some(candidate) = cover_or_next(ranges, display_point, map) {
 241        return Some(
 242            DelimiterRange {
 243                open: candidate.open_range,
 244                close: candidate.close_range,
 245            }
 246            .to_display_range(map, around),
 247        );
 248    }
 249
 250    // Fall back to innermost enclosing brackets
 251    let (open_bracket, close_bracket) = buffer
 252        .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?;
 253
 254    Some(
 255        DelimiterRange {
 256            open: excerpt.map_range_from_buffer(
 257                BufferOffset(open_bracket.start)..BufferOffset(open_bracket.end),
 258            ),
 259            close: excerpt.map_range_from_buffer(
 260                BufferOffset(close_bracket.start)..BufferOffset(close_bracket.end),
 261            ),
 262        }
 263        .to_display_range(map, around),
 264    )
 265}
 266
 267fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range<Point> {
 268    let (start, mut end) = (
 269        map.prev_line_boundary(point).0,
 270        map.next_line_boundary(point).0,
 271    );
 272
 273    if end == point {
 274        end = map.max_point().to_point(map);
 275    }
 276
 277    start..end
 278}
 279
 280fn get_visible_line_range(line_range: &Range<Point>) -> Range<Point> {
 281    let end_column = line_range.end.column.saturating_sub(1);
 282    line_range.start..Point::new(line_range.end.row, end_column)
 283}
 284
 285fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool {
 286    matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`'))
 287}
 288
 289fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool {
 290    matches!(
 291        buffer.chars_at(start).next(),
 292        Some('(' | '[' | '{' | '<' | '|')
 293    )
 294}
 295
 296fn find_mini_quotes(
 297    map: &DisplaySnapshot,
 298    display_point: DisplayPoint,
 299    around: bool,
 300) -> Option<Range<DisplayPoint>> {
 301    find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
 302}
 303
 304fn find_mini_brackets(
 305    map: &DisplaySnapshot,
 306    display_point: DisplayPoint,
 307    around: bool,
 308) -> Option<Range<DisplayPoint>> {
 309    find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
 310}
 311
 312actions!(
 313    vim,
 314    [
 315        /// Selects a sentence text object.
 316        Sentence,
 317        /// Selects a paragraph text object.
 318        Paragraph,
 319        /// Selects text within single quotes.
 320        Quotes,
 321        /// Selects text within backticks.
 322        BackQuotes,
 323        /// Selects text within the nearest quotes (single or double).
 324        MiniQuotes,
 325        /// Selects text within any type of quotes.
 326        AnyQuotes,
 327        /// Selects text within double quotes.
 328        DoubleQuotes,
 329        /// Selects text within vertical bars (pipes).
 330        VerticalBars,
 331        /// Selects text within the nearest brackets.
 332        MiniBrackets,
 333        /// Selects text within any type of brackets.
 334        AnyBrackets,
 335        /// Selects a function argument.
 336        Argument,
 337        /// Selects an HTML/XML tag.
 338        Tag,
 339        /// Selects a method or function.
 340        Method,
 341        /// Selects a class definition.
 342        Class,
 343        /// Selects a comment block.
 344        Comment,
 345        /// Selects the entire file.
 346        EntireFile
 347    ]
 348);
 349
 350pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 351    Vim::action(
 352        editor,
 353        cx,
 354        |vim, &Word { ignore_punctuation }: &Word, window, cx| {
 355            vim.object(Object::Word { ignore_punctuation }, window, cx)
 356        },
 357    );
 358    Vim::action(
 359        editor,
 360        cx,
 361        |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
 362            vim.object(Object::Subword { ignore_punctuation }, window, cx)
 363        },
 364    );
 365    Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
 366        vim.object(Object::Tag, window, cx)
 367    });
 368    Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
 369        vim.object(Object::Sentence, window, cx)
 370    });
 371    Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
 372        vim.object(Object::Paragraph, window, cx)
 373    });
 374    Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
 375        vim.object(Object::Quotes, window, cx)
 376    });
 377    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
 378        vim.object(Object::BackQuotes, window, cx)
 379    });
 380    Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
 381        vim.object(Object::MiniQuotes, window, cx)
 382    });
 383    Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
 384        vim.object(Object::MiniBrackets, window, cx)
 385    });
 386    Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
 387        vim.object(Object::AnyQuotes, window, cx)
 388    });
 389    Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
 390        vim.object(Object::AnyBrackets, window, cx)
 391    });
 392    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
 393        vim.object(Object::BackQuotes, window, cx)
 394    });
 395    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 396        vim.object(Object::DoubleQuotes, window, cx)
 397    });
 398    Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
 399        vim.object_impl(Object::Parentheses, action.opening, window, cx)
 400    });
 401    Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
 402        vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
 403    });
 404    Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
 405        vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
 406    });
 407    Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
 408        vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
 409    });
 410    Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
 411        vim.object(Object::VerticalBars, window, cx)
 412    });
 413    Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
 414        vim.object(Object::Argument, window, cx)
 415    });
 416    Vim::action(editor, cx, |vim, _: &Method, window, cx| {
 417        vim.object(Object::Method, window, cx)
 418    });
 419    Vim::action(editor, cx, |vim, _: &Class, window, cx| {
 420        vim.object(Object::Class, window, cx)
 421    });
 422    Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
 423        vim.object(Object::EntireFile, window, cx)
 424    });
 425    Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
 426        if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
 427            vim.push_operator(Operator::Object { around: true }, window, cx);
 428        }
 429        vim.object(Object::Comment, window, cx)
 430    });
 431    Vim::action(
 432        editor,
 433        cx,
 434        |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
 435            vim.object(Object::IndentObj { include_below }, window, cx)
 436        },
 437    );
 438}
 439
 440impl Vim {
 441    fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 442        self.object_impl(object, false, window, cx);
 443    }
 444
 445    fn object_impl(
 446        &mut self,
 447        object: Object,
 448        opening: bool,
 449        window: &mut Window,
 450        cx: &mut Context<Self>,
 451    ) {
 452        let count = Self::take_count(cx);
 453
 454        match self.mode {
 455            Mode::Normal | Mode::HelixNormal => {
 456                self.normal_object(object, count, opening, window, cx)
 457            }
 458            Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
 459                self.visual_object(object, count, window, cx)
 460            }
 461            Mode::Insert | Mode::Replace => {
 462                // Shouldn't execute a text object in insert mode. Ignoring
 463            }
 464        }
 465    }
 466}
 467
 468impl Object {
 469    pub fn is_multiline(self) -> bool {
 470        match self {
 471            Object::Word { .. }
 472            | Object::Subword { .. }
 473            | Object::Quotes
 474            | Object::BackQuotes
 475            | Object::AnyQuotes
 476            | Object::MiniQuotes
 477            | Object::VerticalBars
 478            | Object::DoubleQuotes => false,
 479            Object::Sentence
 480            | Object::Paragraph
 481            | Object::AnyBrackets
 482            | Object::MiniBrackets
 483            | Object::Parentheses
 484            | Object::Tag
 485            | Object::AngleBrackets
 486            | Object::CurlyBrackets
 487            | Object::SquareBrackets
 488            | Object::Argument
 489            | Object::Method
 490            | Object::Class
 491            | Object::EntireFile
 492            | Object::Comment
 493            | Object::IndentObj { .. } => true,
 494        }
 495    }
 496
 497    pub fn always_expands_both_ways(self) -> bool {
 498        match self {
 499            Object::Word { .. }
 500            | Object::Subword { .. }
 501            | Object::Sentence
 502            | Object::Paragraph
 503            | Object::Argument
 504            | Object::IndentObj { .. } => false,
 505            Object::Quotes
 506            | Object::BackQuotes
 507            | Object::AnyQuotes
 508            | Object::MiniQuotes
 509            | Object::DoubleQuotes
 510            | Object::VerticalBars
 511            | Object::AnyBrackets
 512            | Object::MiniBrackets
 513            | Object::Parentheses
 514            | Object::SquareBrackets
 515            | Object::Tag
 516            | Object::Method
 517            | Object::Class
 518            | Object::Comment
 519            | Object::EntireFile
 520            | Object::CurlyBrackets
 521            | Object::AngleBrackets => true,
 522        }
 523    }
 524
 525    pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
 526        match self {
 527            Object::Word { .. }
 528            | Object::Subword { .. }
 529            | Object::Sentence
 530            | Object::Quotes
 531            | Object::AnyQuotes
 532            | Object::MiniQuotes
 533            | Object::BackQuotes
 534            | Object::DoubleQuotes => {
 535                if current_mode == Mode::VisualBlock {
 536                    Mode::VisualBlock
 537                } else {
 538                    Mode::Visual
 539                }
 540            }
 541            Object::Parentheses
 542            | Object::AnyBrackets
 543            | Object::MiniBrackets
 544            | Object::SquareBrackets
 545            | Object::CurlyBrackets
 546            | Object::AngleBrackets
 547            | Object::VerticalBars
 548            | Object::Tag
 549            | Object::Comment
 550            | Object::Argument
 551            | Object::IndentObj { .. } => Mode::Visual,
 552            Object::Method | Object::Class => {
 553                if around {
 554                    Mode::VisualLine
 555                } else {
 556                    Mode::Visual
 557                }
 558            }
 559            Object::Paragraph | Object::EntireFile => Mode::VisualLine,
 560        }
 561    }
 562
 563    pub fn range(
 564        self,
 565        map: &DisplaySnapshot,
 566        selection: Selection<DisplayPoint>,
 567        around: bool,
 568        times: Option<usize>,
 569    ) -> Option<Range<DisplayPoint>> {
 570        let relative_to = selection.head();
 571        match self {
 572            Object::Word { ignore_punctuation } => {
 573                let count = times.unwrap_or(1);
 574                if around {
 575                    around_word(map, relative_to, ignore_punctuation, count)
 576                } else {
 577                    in_word(map, relative_to, ignore_punctuation, count).map(|range| {
 578                        // For iw with count > 1, vim includes trailing whitespace
 579                        if count > 1 {
 580                            let spans_multiple_lines = range.start.row() != range.end.row();
 581                            expand_to_include_whitespace(map, range, !spans_multiple_lines)
 582                        } else {
 583                            range
 584                        }
 585                    })
 586                }
 587            }
 588            Object::Subword { ignore_punctuation } => {
 589                if around {
 590                    around_subword(map, relative_to, ignore_punctuation)
 591                } else {
 592                    in_subword(map, relative_to, ignore_punctuation)
 593                }
 594            }
 595            Object::Sentence => sentence(map, relative_to, around),
 596            //change others later
 597            Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
 598            Object::Quotes => {
 599                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 600            }
 601            Object::BackQuotes => {
 602                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 603            }
 604            Object::AnyQuotes => {
 605                let quote_types = ['\'', '"', '`'];
 606                let cursor_offset = relative_to.to_offset(map, Bias::Left);
 607
 608                // Find innermost range directly without collecting all ranges
 609                let mut innermost = None;
 610                let mut min_size = usize::MAX;
 611
 612                // First pass: find innermost enclosing range
 613                for quote in quote_types {
 614                    if let Some(range) = surrounding_markers(
 615                        map,
 616                        relative_to,
 617                        around,
 618                        self.is_multiline(),
 619                        quote,
 620                        quote,
 621                    ) {
 622                        let start_offset = range.start.to_offset(map, Bias::Left);
 623                        let end_offset = range.end.to_offset(map, Bias::Right);
 624
 625                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
 626                            let size = end_offset - start_offset;
 627                            if size < min_size {
 628                                min_size = size;
 629                                innermost = Some(range);
 630                            }
 631                        }
 632                    }
 633                }
 634
 635                if let Some(range) = innermost {
 636                    return Some(range);
 637                }
 638
 639                // Fallback: find nearest pair if not inside any quotes
 640                quote_types
 641                    .iter()
 642                    .flat_map(|&quote| {
 643                        surrounding_markers(
 644                            map,
 645                            relative_to,
 646                            around,
 647                            self.is_multiline(),
 648                            quote,
 649                            quote,
 650                        )
 651                    })
 652                    .min_by_key(|range| {
 653                        let start_offset = range.start.to_offset(map, Bias::Left);
 654                        let end_offset = range.end.to_offset(map, Bias::Right);
 655                        if cursor_offset < start_offset {
 656                            (start_offset - cursor_offset) as isize
 657                        } else if cursor_offset > end_offset {
 658                            (cursor_offset - end_offset) as isize
 659                        } else {
 660                            0
 661                        }
 662                    })
 663            }
 664            Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
 665            Object::DoubleQuotes => {
 666                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 667            }
 668            Object::VerticalBars => {
 669                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 670            }
 671            Object::Parentheses => {
 672                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 673            }
 674            Object::Tag => {
 675                let head = selection.head();
 676                let range = selection.range();
 677                surrounding_html_tag(map, head, range, around)
 678            }
 679            Object::AnyBrackets => {
 680                let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
 681                let cursor_offset = relative_to.to_offset(map, Bias::Left);
 682
 683                // Find innermost enclosing bracket range
 684                let mut innermost = None;
 685                let mut min_size = usize::MAX;
 686
 687                for &(open, close) in bracket_pairs.iter() {
 688                    if let Some(range) = surrounding_markers(
 689                        map,
 690                        relative_to,
 691                        around,
 692                        self.is_multiline(),
 693                        open,
 694                        close,
 695                    ) {
 696                        let start_offset = range.start.to_offset(map, Bias::Left);
 697                        let end_offset = range.end.to_offset(map, Bias::Right);
 698
 699                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
 700                            let size = end_offset - start_offset;
 701                            if size < min_size {
 702                                min_size = size;
 703                                innermost = Some(range);
 704                            }
 705                        }
 706                    }
 707                }
 708
 709                if let Some(range) = innermost {
 710                    return Some(range);
 711                }
 712
 713                // Fallback: find nearest bracket pair if not inside any
 714                bracket_pairs
 715                    .iter()
 716                    .flat_map(|&(open, close)| {
 717                        surrounding_markers(
 718                            map,
 719                            relative_to,
 720                            around,
 721                            self.is_multiline(),
 722                            open,
 723                            close,
 724                        )
 725                    })
 726                    .min_by_key(|range| {
 727                        let start_offset = range.start.to_offset(map, Bias::Left);
 728                        let end_offset = range.end.to_offset(map, Bias::Right);
 729                        if cursor_offset < start_offset {
 730                            (start_offset - cursor_offset) as isize
 731                        } else if cursor_offset > end_offset {
 732                            (cursor_offset - end_offset) as isize
 733                        } else {
 734                            0
 735                        }
 736                    })
 737            }
 738            Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
 739            Object::SquareBrackets => {
 740                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 741            }
 742            Object::CurlyBrackets => {
 743                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 744            }
 745            Object::AngleBrackets => {
 746                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 747            }
 748            Object::Method => text_object(
 749                map,
 750                relative_to,
 751                if around {
 752                    TextObject::AroundFunction
 753                } else {
 754                    TextObject::InsideFunction
 755                },
 756            ),
 757            Object::Comment => text_object(
 758                map,
 759                relative_to,
 760                if around {
 761                    TextObject::AroundComment
 762                } else {
 763                    TextObject::InsideComment
 764                },
 765            ),
 766            Object::Class => text_object(
 767                map,
 768                relative_to,
 769                if around {
 770                    TextObject::AroundClass
 771                } else {
 772                    TextObject::InsideClass
 773                },
 774            ),
 775            Object::Argument => argument(map, relative_to, around),
 776            Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
 777            Object::EntireFile => entire_file(map),
 778        }
 779    }
 780
 781    pub fn expand_selection(
 782        self,
 783        map: &DisplaySnapshot,
 784        selection: &mut Selection<DisplayPoint>,
 785        around: bool,
 786        times: Option<usize>,
 787    ) -> bool {
 788        if let Some(range) = self.range(map, selection.clone(), around, times) {
 789            selection.start = range.start;
 790            selection.end = range.end;
 791            true
 792        } else {
 793            false
 794        }
 795    }
 796}
 797
 798/// Returns a range that surrounds the word `relative_to` is in.
 799///
 800/// If `relative_to` is at the start of a word, return the word.
 801/// If `relative_to` is between words, return the space between.
 802/// If `times` > 1, extend to include additional words.
 803fn in_word(
 804    map: &DisplaySnapshot,
 805    relative_to: DisplayPoint,
 806    ignore_punctuation: bool,
 807    times: usize,
 808) -> Option<Range<DisplayPoint>> {
 809    // Use motion::right so that we consider the character under the cursor when looking for the start
 810    let classifier = map
 811        .buffer_snapshot()
 812        .char_classifier_at(relative_to.to_point(map))
 813        .ignore_punctuation(ignore_punctuation);
 814    let start = movement::find_preceding_boundary_display_point(
 815        map,
 816        right(map, relative_to, 1),
 817        movement::FindRange::SingleLine,
 818        &mut |left, right| classifier.kind(left) != classifier.kind(right),
 819    );
 820
 821    let mut end = movement::find_boundary(
 822        map,
 823        relative_to,
 824        FindRange::SingleLine,
 825        &mut |left, right| classifier.kind(left) != classifier.kind(right),
 826    );
 827
 828    let mut is_boundary = |left: char, right: char| classifier.kind(left) != classifier.kind(right);
 829
 830    for _ in 1..times {
 831        let kind_at_end = map
 832            .buffer_chars_at(end.to_offset(map, Bias::Right))
 833            .next()
 834            .map(|(c, _)| classifier.kind(c));
 835
 836        // Skip whitespace but not punctuation (punctuation is its own word unit).
 837        let next_end = if kind_at_end == Some(CharKind::Whitespace) {
 838            let after_whitespace =
 839                movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary);
 840            movement::find_boundary(
 841                map,
 842                after_whitespace,
 843                FindRange::MultiLine,
 844                &mut is_boundary,
 845            )
 846        } else {
 847            movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary)
 848        };
 849        if next_end == end {
 850            break;
 851        }
 852        end = next_end;
 853    }
 854
 855    Some(start..end)
 856}
 857
 858fn in_subword(
 859    map: &DisplaySnapshot,
 860    relative_to: DisplayPoint,
 861    ignore_punctuation: bool,
 862) -> Option<Range<DisplayPoint>> {
 863    let offset = relative_to.to_offset(map, Bias::Left);
 864    // Use motion::right so that we consider the character under the cursor when looking for the start
 865    let classifier = map
 866        .buffer_snapshot()
 867        .char_classifier_at(relative_to.to_point(map))
 868        .ignore_punctuation(ignore_punctuation);
 869    let in_subword = map
 870        .buffer_chars_at(offset)
 871        .next()
 872        .map(|(c, _)| {
 873            let is_separator = "._-".contains(c);
 874            !classifier.is_whitespace(c) && !is_separator
 875        })
 876        .unwrap_or(false);
 877
 878    let start = if in_subword {
 879        movement::find_preceding_boundary_display_point(
 880            map,
 881            right(map, relative_to, 1),
 882            movement::FindRange::SingleLine,
 883            &mut |left, right| {
 884                let is_word_start = classifier.kind(left) != classifier.kind(right);
 885                is_word_start || is_subword_start(left, right, "._-")
 886            },
 887        )
 888    } else {
 889        movement::find_boundary(
 890            map,
 891            relative_to,
 892            FindRange::SingleLine,
 893            &mut |left, right| {
 894                let is_word_start = classifier.kind(left) != classifier.kind(right);
 895                is_word_start || is_subword_start(left, right, "._-")
 896            },
 897        )
 898    };
 899
 900    let end = movement::find_boundary(
 901        map,
 902        relative_to,
 903        FindRange::SingleLine,
 904        &mut |left, right| {
 905            let is_word_end = classifier.kind(left) != classifier.kind(right);
 906            is_word_end || is_subword_end(left, right, "._-")
 907        },
 908    );
 909
 910    Some(start..end)
 911}
 912
 913pub fn surrounding_html_tag(
 914    map: &DisplaySnapshot,
 915    head: DisplayPoint,
 916    range: Range<DisplayPoint>,
 917    around: bool,
 918) -> Option<Range<DisplayPoint>> {
 919    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 920        chars
 921            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 922            .collect()
 923    }
 924    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 925        if Some('<') != chars.next() {
 926            return None;
 927        }
 928        Some(read_tag(chars))
 929    }
 930    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 931        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 932            return None;
 933        }
 934        Some(read_tag(chars))
 935    }
 936
 937    let snapshot = &map.buffer_snapshot();
 938    let offset = head.to_offset(map, Bias::Left);
 939    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 940    let buffer = excerpt.buffer();
 941    let offset = excerpt.map_offset_to_buffer(offset);
 942
 943    // Find the most closest to current offset
 944    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 945    let mut last_child_node = cursor.node();
 946    while cursor.goto_first_child_for_byte(offset.0).is_some() {
 947        last_child_node = cursor.node();
 948    }
 949
 950    let mut last_child_node = Some(last_child_node);
 951    while let Some(cur_node) = last_child_node {
 952        if cur_node.child_count() >= 2 {
 953            let first_child = cur_node.child(0);
 954            let last_child = cur_node.child(cur_node.child_count() as u32 - 1);
 955            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 956                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 957                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 958                // It needs to be handled differently according to the selection length
 959                let is_valid = if range.end.to_offset(map, Bias::Left)
 960                    - range.start.to_offset(map, Bias::Left)
 961                    <= 1
 962                {
 963                    offset.0 <= last_child.end_byte()
 964                } else {
 965                    excerpt
 966                        .map_offset_to_buffer(range.start.to_offset(map, Bias::Left))
 967                        .0
 968                        >= first_child.start_byte()
 969                        && excerpt
 970                            .map_offset_to_buffer(range.end.to_offset(map, Bias::Left))
 971                            .0
 972                            <= last_child.start_byte() + 1
 973                };
 974                if open_tag.is_some() && open_tag == close_tag && is_valid {
 975                    let range = if around {
 976                        first_child.byte_range().start..last_child.byte_range().end
 977                    } else {
 978                        first_child.byte_range().end..last_child.byte_range().start
 979                    };
 980                    let range = BufferOffset(range.start)..BufferOffset(range.end);
 981                    if excerpt.contains_buffer_range(range.clone()) {
 982                        let result = excerpt.map_range_from_buffer(range);
 983                        return Some(
 984                            result.start.to_display_point(map)..result.end.to_display_point(map),
 985                        );
 986                    }
 987                }
 988            }
 989        }
 990        last_child_node = cur_node.parent();
 991    }
 992    None
 993}
 994
 995/// Returns a range that surrounds the word and following whitespace
 996/// relative_to is in.
 997///
 998/// If `relative_to` is at the start of a word, return the word and following whitespace.
 999/// If `relative_to` is between words, return the whitespace back and the following word.
1000///
1001/// if in word
1002///   delete that word
1003///   if there is whitespace following the word, delete that as well
1004///   otherwise, delete any preceding whitespace
1005/// otherwise
1006///   delete whitespace around cursor
1007///   delete word following the cursor
1008/// If `times` > 1, extend to include additional words.
1009fn around_word(
1010    map: &DisplaySnapshot,
1011    relative_to: DisplayPoint,
1012    ignore_punctuation: bool,
1013    times: usize,
1014) -> Option<Range<DisplayPoint>> {
1015    let offset = relative_to.to_offset(map, Bias::Left);
1016    let classifier = map
1017        .buffer_snapshot()
1018        .char_classifier_at(offset)
1019        .ignore_punctuation(ignore_punctuation);
1020    let in_word = map
1021        .buffer_chars_at(offset)
1022        .next()
1023        .map(|(c, _)| !classifier.is_whitespace(c))
1024        .unwrap_or(false);
1025
1026    if in_word {
1027        around_containing_word(map, relative_to, ignore_punctuation, times)
1028    } else {
1029        around_next_word(map, relative_to, ignore_punctuation, times)
1030    }
1031}
1032
1033fn around_subword(
1034    map: &DisplaySnapshot,
1035    relative_to: DisplayPoint,
1036    ignore_punctuation: bool,
1037) -> Option<Range<DisplayPoint>> {
1038    // Use motion::right so that we consider the character under the cursor when looking for the start
1039    let classifier = map
1040        .buffer_snapshot()
1041        .char_classifier_at(relative_to.to_point(map))
1042        .ignore_punctuation(ignore_punctuation);
1043    let start = movement::find_preceding_boundary_display_point(
1044        map,
1045        right(map, relative_to, 1),
1046        movement::FindRange::SingleLine,
1047        &mut |left, right| {
1048            let is_separator = |c: char| "._-".contains(c);
1049            let is_word_start =
1050                classifier.kind(left) != classifier.kind(right) && !is_separator(left);
1051            is_word_start || is_subword_start(left, right, "._-")
1052        },
1053    );
1054
1055    let end = movement::find_boundary(
1056        map,
1057        relative_to,
1058        FindRange::SingleLine,
1059        &mut |left, right| {
1060            let is_separator = |c: char| "._-".contains(c);
1061            let is_word_end =
1062                classifier.kind(left) != classifier.kind(right) && !is_separator(right);
1063            is_word_end || is_subword_end(left, right, "._-")
1064        },
1065    );
1066
1067    Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
1068}
1069
1070fn around_containing_word(
1071    map: &DisplaySnapshot,
1072    relative_to: DisplayPoint,
1073    ignore_punctuation: bool,
1074    times: usize,
1075) -> Option<Range<DisplayPoint>> {
1076    in_word(map, relative_to, ignore_punctuation, times).map(|range| {
1077        let spans_multiple_lines = range.start.row() != range.end.row();
1078        let stop_at_newline = !spans_multiple_lines;
1079
1080        let line_start = DisplayPoint::new(range.start.row(), 0);
1081        let is_first_word = map
1082            .buffer_chars_at(line_start.to_offset(map, Bias::Left))
1083            .take_while(|(ch, offset)| {
1084                offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
1085            })
1086            .count()
1087            > 0;
1088
1089        if is_first_word {
1090            // For first word on line, trim indentation
1091            let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
1092            expanded.start = range.start;
1093            expanded
1094        } else {
1095            expand_to_include_whitespace(map, range, stop_at_newline)
1096        }
1097    })
1098}
1099
1100fn around_next_word(
1101    map: &DisplaySnapshot,
1102    relative_to: DisplayPoint,
1103    ignore_punctuation: bool,
1104    times: usize,
1105) -> Option<Range<DisplayPoint>> {
1106    let classifier = map
1107        .buffer_snapshot()
1108        .char_classifier_at(relative_to.to_point(map))
1109        .ignore_punctuation(ignore_punctuation);
1110    let start = movement::find_preceding_boundary_display_point(
1111        map,
1112        right(map, relative_to, 1),
1113        FindRange::SingleLine,
1114        &mut |left, right| classifier.kind(left) != classifier.kind(right),
1115    );
1116
1117    let mut word_found = false;
1118    let mut end = movement::find_boundary(
1119        map,
1120        relative_to,
1121        FindRange::MultiLine,
1122        &mut |left, right| {
1123            let left_kind = classifier.kind(left);
1124            let right_kind = classifier.kind(right);
1125
1126            let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
1127
1128            if right_kind != CharKind::Whitespace {
1129                word_found = true;
1130            }
1131
1132            found
1133        },
1134    );
1135
1136    for _ in 1..times {
1137        let next_end =
1138            movement::find_boundary(map, end, FindRange::MultiLine, &mut |left, right| {
1139                let left_kind = classifier.kind(left);
1140                let right_kind = classifier.kind(right);
1141
1142                let in_word_unit = left_kind != CharKind::Whitespace;
1143                (in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
1144            });
1145        if next_end == end {
1146            break;
1147        }
1148        end = next_end;
1149    }
1150
1151    Some(start..end)
1152}
1153
1154fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
1155    Some(DisplayPoint::zero()..map.max_point())
1156}
1157
1158fn text_object(
1159    map: &DisplaySnapshot,
1160    relative_to: DisplayPoint,
1161    target: TextObject,
1162) -> Option<Range<DisplayPoint>> {
1163    let snapshot = &map.buffer_snapshot();
1164    let offset = relative_to.to_offset(map, Bias::Left);
1165
1166    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1167    let buffer = excerpt.buffer();
1168    let offset = excerpt.map_offset_to_buffer(offset);
1169
1170    let mut matches: Vec<Range<usize>> = buffer
1171        .text_object_ranges(offset..offset, TreeSitterOptions::default())
1172        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1173        .collect();
1174    matches.sort_by_key(|r| r.end - r.start);
1175    if let Some(buffer_range) = matches.first() {
1176        let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1177        let range = excerpt.map_range_from_buffer(buffer_range);
1178        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1179    }
1180
1181    let around = target.around()?;
1182    let mut matches: Vec<Range<usize>> = buffer
1183        .text_object_ranges(offset..offset, TreeSitterOptions::default())
1184        .filter_map(|(r, m)| if m == around { Some(r) } else { None })
1185        .collect();
1186    matches.sort_by_key(|r| r.end - r.start);
1187    let around_range = matches.first()?;
1188
1189    let mut matches: Vec<Range<usize>> = buffer
1190        .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
1191        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1192        .collect();
1193    matches.sort_by_key(|r| r.start);
1194    if let Some(buffer_range) = matches.first()
1195        && !buffer_range.is_empty()
1196    {
1197        let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1198        let range = excerpt.map_range_from_buffer(buffer_range);
1199        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1200    }
1201    let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end);
1202    let buffer_range = excerpt.map_range_from_buffer(around_range);
1203    return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
1204}
1205
1206fn argument(
1207    map: &DisplaySnapshot,
1208    relative_to: DisplayPoint,
1209    around: bool,
1210) -> Option<Range<DisplayPoint>> {
1211    let snapshot = &map.buffer_snapshot();
1212    let offset = relative_to.to_offset(map, Bias::Left);
1213
1214    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
1215    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1216    let buffer = excerpt.buffer();
1217
1218    fn comma_delimited_range_at(
1219        buffer: &BufferSnapshot,
1220        mut offset: BufferOffset,
1221        include_comma: bool,
1222    ) -> Option<Range<BufferOffset>> {
1223        // Seek to the first non-whitespace character
1224        offset += buffer
1225            .chars_at(offset)
1226            .take_while(|c| c.is_whitespace())
1227            .map(char::len_utf8)
1228            .sum::<usize>();
1229
1230        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
1231            // Filter out empty ranges
1232            if open.end == close.start {
1233                return false;
1234            }
1235
1236            // If the cursor is outside the brackets, ignore them
1237            if open.start == offset.0 || close.end == offset.0 {
1238                return false;
1239            }
1240
1241            // TODO: Is there any better way to filter out string brackets?
1242            // Used to filter out string brackets
1243            matches!(
1244                buffer.chars_at(open.start).next(),
1245                Some('(' | '[' | '{' | '<' | '|')
1246            )
1247        };
1248
1249        // Find the brackets containing the cursor
1250        let (open_bracket, close_bracket) =
1251            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
1252
1253        let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
1254
1255        let layer = buffer.syntax_layer_at(offset)?;
1256        let node = layer.node();
1257        let mut cursor = node.walk();
1258
1259        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
1260        let mut parent_covers_bracket_range = false;
1261        loop {
1262            let node = cursor.node();
1263            let range = node.byte_range();
1264            let covers_bracket_range =
1265                range.start == open_bracket.start && range.end == close_bracket.end;
1266            if parent_covers_bracket_range && !covers_bracket_range {
1267                break;
1268            }
1269            parent_covers_bracket_range = covers_bracket_range;
1270
1271            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
1272            cursor.goto_first_child_for_byte(offset.0)?;
1273        }
1274
1275        let mut argument_node = cursor.node();
1276
1277        // If the child node is the open bracket, move to the next sibling.
1278        if argument_node.byte_range() == open_bracket {
1279            if !cursor.goto_next_sibling() {
1280                return Some(inner_bracket_range);
1281            }
1282            argument_node = cursor.node();
1283        }
1284        // While the child node is the close bracket or a comma, move to the previous sibling
1285        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
1286            if !cursor.goto_previous_sibling() {
1287                return Some(inner_bracket_range);
1288            }
1289            argument_node = cursor.node();
1290            if argument_node.byte_range() == open_bracket {
1291                return Some(inner_bracket_range);
1292            }
1293        }
1294
1295        // The start and end of the argument range, defaulting to the start and end of the argument node
1296        let mut start = argument_node.start_byte();
1297        let mut end = argument_node.end_byte();
1298
1299        let mut needs_surrounding_comma = include_comma;
1300
1301        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
1302        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
1303        while cursor.goto_previous_sibling() {
1304            let prev = cursor.node();
1305
1306            if prev.start_byte() < open_bracket.end {
1307                start = open_bracket.end;
1308                break;
1309            } else if prev.kind() == "," {
1310                if needs_surrounding_comma {
1311                    start = prev.start_byte();
1312                    needs_surrounding_comma = false;
1313                }
1314                break;
1315            } else if prev.start_byte() < start {
1316                start = prev.start_byte();
1317            }
1318        }
1319
1320        // Do the same for the end of the argument, extending to next comma or the end of the argument list
1321        while cursor.goto_next_sibling() {
1322            let next = cursor.node();
1323
1324            if next.end_byte() > close_bracket.start {
1325                end = close_bracket.start;
1326                break;
1327            } else if next.kind() == "," {
1328                if needs_surrounding_comma {
1329                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
1330                    if let Some(next_arg) = next.next_sibling() {
1331                        end = next_arg.start_byte();
1332                    } else {
1333                        end = next.end_byte();
1334                    }
1335                }
1336                break;
1337            } else if next.end_byte() > end {
1338                end = next.end_byte();
1339            }
1340        }
1341
1342        Some(BufferOffset(start)..BufferOffset(end))
1343    }
1344
1345    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
1346
1347    if excerpt.contains_buffer_range(result.clone()) {
1348        let result = excerpt.map_range_from_buffer(result);
1349        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
1350    } else {
1351        None
1352    }
1353}
1354
1355fn indent(
1356    map: &DisplaySnapshot,
1357    relative_to: DisplayPoint,
1358    around: bool,
1359    include_below: bool,
1360) -> Option<Range<DisplayPoint>> {
1361    let point = relative_to.to_point(map);
1362    let row = point.row;
1363
1364    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
1365
1366    // Loop backwards until we find a non-blank line with less indent
1367    let mut start_row = row;
1368    for prev_row in (0..row).rev() {
1369        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
1370        if indent.is_line_empty() {
1371            continue;
1372        }
1373        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1374            if around {
1375                // When around is true, include the first line with less indent
1376                start_row = prev_row;
1377            }
1378            break;
1379        }
1380        start_row = prev_row;
1381    }
1382
1383    // Loop forwards until we find a non-blank line with less indent
1384    let mut end_row = row;
1385    let max_rows = map.buffer_snapshot().max_row().0;
1386    for next_row in (row + 1)..=max_rows {
1387        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
1388        if indent.is_line_empty() {
1389            continue;
1390        }
1391        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1392            if around && include_below {
1393                // When around is true and including below, include this line
1394                end_row = next_row;
1395            }
1396            break;
1397        }
1398        end_row = next_row;
1399    }
1400
1401    let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1402    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1403    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1404    Some(start..end)
1405}
1406
1407fn sentence(
1408    map: &DisplaySnapshot,
1409    relative_to: DisplayPoint,
1410    around: bool,
1411) -> Option<Range<DisplayPoint>> {
1412    let mut start = None;
1413    let relative_offset = relative_to.to_offset(map, Bias::Left);
1414    let mut previous_end = relative_offset;
1415
1416    let mut chars = map.buffer_chars_at(previous_end).peekable();
1417
1418    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1419    for (char, offset) in chars
1420        .peek()
1421        .cloned()
1422        .into_iter()
1423        .chain(map.reverse_buffer_chars_at(previous_end))
1424    {
1425        if is_sentence_end(map, offset) {
1426            break;
1427        }
1428
1429        if is_possible_sentence_start(char) {
1430            start = Some(offset);
1431        }
1432
1433        previous_end = offset;
1434    }
1435
1436    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1437    let mut end = relative_offset;
1438    for (char, offset) in chars {
1439        if start.is_none() && is_possible_sentence_start(char) {
1440            if around {
1441                start = Some(offset);
1442                continue;
1443            } else {
1444                end = offset;
1445                break;
1446            }
1447        }
1448
1449        if char != '\n' {
1450            end = offset + char.len_utf8();
1451        }
1452
1453        if is_sentence_end(map, end) {
1454            break;
1455        }
1456    }
1457
1458    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1459    if around {
1460        range = expand_to_include_whitespace(map, range, false);
1461    }
1462
1463    Some(range)
1464}
1465
1466fn is_possible_sentence_start(character: char) -> bool {
1467    !character.is_whitespace() && character != '.'
1468}
1469
1470const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1471const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1472const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1473fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
1474    let mut next_chars = map.buffer_chars_at(offset).peekable();
1475    if let Some((char, _)) = next_chars.next() {
1476        // We are at a double newline. This position is a sentence end.
1477        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1478            return true;
1479        }
1480
1481        // The next text is not a valid whitespace. This is not a sentence end
1482        if !SENTENCE_END_WHITESPACE.contains(&char) {
1483            return false;
1484        }
1485    }
1486
1487    for (char, _) in map.reverse_buffer_chars_at(offset) {
1488        if SENTENCE_END_PUNCTUATION.contains(&char) {
1489            return true;
1490        }
1491
1492        if !SENTENCE_END_FILLERS.contains(&char) {
1493            return false;
1494        }
1495    }
1496
1497    false
1498}
1499
1500/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1501/// whitespace to the end first and falls back to the start if there was none.
1502pub fn expand_to_include_whitespace(
1503    map: &DisplaySnapshot,
1504    range: Range<DisplayPoint>,
1505    stop_at_newline: bool,
1506) -> Range<DisplayPoint> {
1507    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1508    let mut whitespace_included = false;
1509
1510    let chars = map.buffer_chars_at(range.end).peekable();
1511    for (char, offset) in chars {
1512        if char == '\n' && stop_at_newline {
1513            break;
1514        }
1515
1516        if char.is_whitespace() {
1517            if char != '\n' || !stop_at_newline {
1518                range.end = offset + char.len_utf8();
1519                whitespace_included = true;
1520            }
1521        } else {
1522            // Found non whitespace. Quit out.
1523            break;
1524        }
1525    }
1526
1527    if !whitespace_included {
1528        for (char, point) in map.reverse_buffer_chars_at(range.start) {
1529            if char == '\n' && stop_at_newline {
1530                break;
1531            }
1532
1533            if !char.is_whitespace() {
1534                break;
1535            }
1536
1537            range.start = point;
1538        }
1539    }
1540
1541    range.start.to_display_point(map)..range.end.to_display_point(map)
1542}
1543
1544/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1545/// where `relative_to` is in. If `around`, principally returns the range ending
1546/// at the end of the next paragraph.
1547///
1548/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1549/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1550/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1551/// the trailing newline is not subject to subsequent operations).
1552///
1553/// Edge cases:
1554/// - If `around` and if the current paragraph is the last paragraph of the
1555///   file and is blank, then the selection results in an error.
1556/// - If `around` and if the current paragraph is the last paragraph of the
1557///   file and is not blank, then the returned range starts at the start of the
1558///   previous paragraph, if it exists.
1559fn paragraph(
1560    map: &DisplaySnapshot,
1561    relative_to: DisplayPoint,
1562    around: bool,
1563    times: usize,
1564) -> Option<Range<DisplayPoint>> {
1565    let mut paragraph_start = start_of_paragraph(map, relative_to);
1566    let mut paragraph_end = end_of_paragraph(map, relative_to);
1567
1568    for i in 0..times {
1569        let paragraph_end_row = paragraph_end.row();
1570        let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1571        let point = relative_to.to_point(map);
1572        let current_line_is_empty = map
1573            .buffer_snapshot()
1574            .is_line_blank(MultiBufferRow(point.row));
1575
1576        if around {
1577            if paragraph_ends_with_eof {
1578                if current_line_is_empty {
1579                    return None;
1580                }
1581
1582                let paragraph_start_buffer_point = paragraph_start.to_point(map);
1583                if paragraph_start_buffer_point.row != 0 {
1584                    let previous_paragraph_last_line_start =
1585                        Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1586                    paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1587                }
1588            } else {
1589                let paragraph_end_buffer_point = paragraph_end.to_point(map);
1590                let mut start_row = paragraph_end_buffer_point.row + 1;
1591                if i > 0 {
1592                    start_row += 1;
1593                }
1594                let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1595                paragraph_end = end_of_paragraph(map, next_paragraph_start);
1596            }
1597        }
1598    }
1599
1600    let range = paragraph_start..paragraph_end;
1601    Some(range)
1602}
1603
1604/// Returns a position of the start of the current paragraph, where a paragraph
1605/// is defined as a run of non-blank lines or a run of blank lines.
1606pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1607    let point = display_point.to_point(map);
1608    if point.row == 0 {
1609        return DisplayPoint::zero();
1610    }
1611
1612    let is_current_line_blank = map
1613        .buffer_snapshot()
1614        .is_line_blank(MultiBufferRow(point.row));
1615
1616    for row in (0..point.row).rev() {
1617        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1618        if blank != is_current_line_blank {
1619            return Point::new(row + 1, 0).to_display_point(map);
1620        }
1621    }
1622
1623    DisplayPoint::zero()
1624}
1625
1626/// Returns a position of the end of the current paragraph, where a paragraph
1627/// is defined as a run of non-blank lines or a run of blank lines.
1628/// The trailing newline is excluded from the paragraph.
1629pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1630    let point = display_point.to_point(map);
1631    if point.row == map.buffer_snapshot().max_row().0 {
1632        return map.max_point();
1633    }
1634
1635    let is_current_line_blank = map
1636        .buffer_snapshot()
1637        .is_line_blank(MultiBufferRow(point.row));
1638
1639    for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
1640        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1641        if blank != is_current_line_blank {
1642            let previous_row = row - 1;
1643            return Point::new(
1644                previous_row,
1645                map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
1646            )
1647            .to_display_point(map);
1648        }
1649    }
1650
1651    map.max_point()
1652}
1653
1654pub fn surrounding_markers(
1655    map: &DisplaySnapshot,
1656    relative_to: DisplayPoint,
1657    around: bool,
1658    search_across_lines: bool,
1659    open_marker: char,
1660    close_marker: char,
1661) -> Option<Range<DisplayPoint>> {
1662    let point = relative_to.to_offset(map, Bias::Left);
1663
1664    let mut matched_closes = 0;
1665    let mut opening = None;
1666
1667    let mut before_ch = match movement::chars_before(map, point).next() {
1668        Some((ch, _)) => ch,
1669        _ => '\0',
1670    };
1671    if let Some((ch, range)) = movement::chars_after(map, point).next()
1672        && ch == open_marker
1673        && before_ch != '\\'
1674    {
1675        if open_marker == close_marker {
1676            let mut total = 0;
1677            for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1678                if ch == '\n' {
1679                    break;
1680                }
1681                if ch == open_marker && before_ch != '\\' {
1682                    total += 1;
1683                }
1684            }
1685            if total % 2 == 0 {
1686                opening = Some(range)
1687            }
1688        } else {
1689            opening = Some(range)
1690        }
1691    }
1692
1693    if opening.is_none() {
1694        let mut chars_before = movement::chars_before(map, point).peekable();
1695        while let Some((ch, range)) = chars_before.next() {
1696            if ch == '\n' && !search_across_lines {
1697                break;
1698            }
1699
1700            if let Some((before_ch, _)) = chars_before.peek()
1701                && *before_ch == '\\'
1702            {
1703                continue;
1704            }
1705
1706            if ch == open_marker {
1707                if matched_closes == 0 {
1708                    opening = Some(range);
1709                    break;
1710                }
1711                matched_closes -= 1;
1712            } else if ch == close_marker {
1713                matched_closes += 1
1714            }
1715        }
1716    }
1717    if opening.is_none() {
1718        for (ch, range) in movement::chars_after(map, point) {
1719            if before_ch != '\\' {
1720                if ch == open_marker {
1721                    opening = Some(range);
1722                    break;
1723                } else if ch == close_marker {
1724                    break;
1725                }
1726            }
1727
1728            before_ch = ch;
1729        }
1730    }
1731
1732    let mut opening = opening?;
1733
1734    let mut matched_opens = 0;
1735    let mut closing = None;
1736    before_ch = match movement::chars_before(map, opening.end).next() {
1737        Some((ch, _)) => ch,
1738        _ => '\0',
1739    };
1740    for (ch, range) in movement::chars_after(map, opening.end) {
1741        if ch == '\n' && !search_across_lines {
1742            break;
1743        }
1744
1745        if before_ch != '\\' {
1746            if ch == close_marker {
1747                if matched_opens == 0 {
1748                    closing = Some(range);
1749                    break;
1750                }
1751                matched_opens -= 1;
1752            } else if ch == open_marker {
1753                matched_opens += 1;
1754            }
1755        }
1756
1757        before_ch = ch;
1758    }
1759
1760    let mut closing = closing?;
1761
1762    if around && !search_across_lines {
1763        let mut found = false;
1764
1765        for (ch, range) in movement::chars_after(map, closing.end) {
1766            if ch.is_whitespace() && ch != '\n' {
1767                found = true;
1768                closing.end = range.end;
1769            } else {
1770                break;
1771            }
1772        }
1773
1774        if !found {
1775            for (ch, range) in movement::chars_before(map, opening.start) {
1776                if ch.is_whitespace() && ch != '\n' {
1777                    opening.start = range.start
1778                } else {
1779                    break;
1780                }
1781            }
1782        }
1783    }
1784
1785    // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1786    if !around && open_marker != close_marker {
1787        let start_point = opening.end.to_display_point(map);
1788        let end_point = closing.start.to_display_point(map);
1789        let start_offset = start_point.to_offset(map, Bias::Left);
1790        let end_offset = end_point.to_offset(map, Bias::Left);
1791
1792        if start_point.row() != end_point.row()
1793            && map
1794                .buffer_chars_at(start_offset)
1795                .take_while(|(_, offset)| offset < &end_offset)
1796                .any(|(ch, _)| !ch.is_whitespace())
1797        {
1798            let mut first_non_ws = None;
1799            let mut last_non_ws = None;
1800            for (ch, offset) in map.buffer_chars_at(start_offset) {
1801                if !ch.is_whitespace() {
1802                    first_non_ws = Some(offset);
1803                    break;
1804                }
1805            }
1806            for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1807                if !ch.is_whitespace() {
1808                    last_non_ws = Some(offset + ch.len_utf8());
1809                    break;
1810                }
1811            }
1812            if let Some(start) = first_non_ws {
1813                opening.end = start;
1814            }
1815            if let Some(end) = last_non_ws {
1816                closing.start = end;
1817            }
1818        }
1819    }
1820
1821    let result = if around {
1822        opening.start..closing.end
1823    } else {
1824        opening.end..closing.start
1825    };
1826
1827    Some(
1828        map.clip_point(result.start.to_display_point(map), Bias::Left)
1829            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1830    )
1831}
1832
1833#[cfg(test)]
1834mod test {
1835    use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
1836    use gpui::KeyBinding;
1837    use indoc::indoc;
1838    use text::Point;
1839
1840    use crate::{
1841        object::{AnyBrackets, AnyQuotes, MiniBrackets},
1842        state::Mode,
1843        test::{NeovimBackedTestContext, VimTestContext},
1844    };
1845
1846    const WORD_LOCATIONS: &str = indoc! {"
1847        The quick ˇbrowˇnˇ•••
1848        fox ˇjuˇmpsˇ over
1849        the lazy dogˇ••
1850        ˇ
1851        ˇ
1852        ˇ
1853        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1854        ˇ••
1855        ˇ••
1856        ˇ  fox-jumpˇs over
1857        the lazy dogˇ•
1858        ˇ
1859        "
1860    };
1861
1862    #[gpui::test]
1863    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1864        let mut cx = NeovimBackedTestContext::new(cx).await;
1865
1866        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1867            .await
1868            .assert_matches();
1869        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1870            .await
1871            .assert_matches();
1872        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1873            .await
1874            .assert_matches();
1875        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1876            .await
1877            .assert_matches();
1878    }
1879
1880    #[gpui::test]
1881    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1882        let mut cx = NeovimBackedTestContext::new(cx).await;
1883
1884        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1885            .await
1886            .assert_matches();
1887        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1888            .await
1889            .assert_matches();
1890        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1891            .await
1892            .assert_matches();
1893        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1894            .await
1895            .assert_matches();
1896    }
1897
1898    #[gpui::test]
1899    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1900        let mut cx = NeovimBackedTestContext::new(cx).await;
1901
1902        /*
1903                cx.set_shared_state("The quick ˇbrown\nfox").await;
1904                cx.simulate_shared_keystrokes(["v"]).await;
1905                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1906                cx.simulate_shared_keystrokes(["i", "w"]).await;
1907                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1908        */
1909        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1910        cx.simulate_shared_keystrokes("v").await;
1911        cx.shared_state()
1912            .await
1913            .assert_eq("The quick brown\n«\nˇ»fox");
1914        cx.simulate_shared_keystrokes("i w").await;
1915        cx.shared_state()
1916            .await
1917            .assert_eq("The quick brown\n«\nˇ»fox");
1918
1919        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1920            .await
1921            .assert_matches();
1922        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1923            .await
1924            .assert_matches();
1925    }
1926
1927    #[gpui::test]
1928    async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
1929        let mut cx = NeovimBackedTestContext::new(cx).await;
1930
1931        cx.set_shared_state("ˇone two three four").await;
1932        cx.simulate_shared_keystrokes("2 d a w").await;
1933        cx.shared_state().await.assert_matches();
1934
1935        cx.set_shared_state("ˇone two three four").await;
1936        cx.simulate_shared_keystrokes("d 2 a w").await;
1937        cx.shared_state().await.assert_matches();
1938
1939        // WORD (shift-w) ignores punctuation
1940        cx.set_shared_state("ˇone-two three-four five").await;
1941        cx.simulate_shared_keystrokes("2 d a shift-w").await;
1942        cx.shared_state().await.assert_matches();
1943
1944        cx.set_shared_state("ˇone two three four five").await;
1945        cx.simulate_shared_keystrokes("3 d a w").await;
1946        cx.shared_state().await.assert_matches();
1947
1948        // Multiplied counts: 2d2aw deletes 4 words (2*2)
1949        cx.set_shared_state("ˇone two three four five six").await;
1950        cx.simulate_shared_keystrokes("2 d 2 a w").await;
1951        cx.shared_state().await.assert_matches();
1952
1953        cx.set_shared_state("ˇone two three four").await;
1954        cx.simulate_shared_keystrokes("2 c a w").await;
1955        cx.shared_state().await.assert_matches();
1956
1957        cx.set_shared_state("ˇone two three four").await;
1958        cx.simulate_shared_keystrokes("2 y a w p").await;
1959        cx.shared_state().await.assert_matches();
1960
1961        // Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
1962        cx.set_shared_state("  ˇfoo-bar baz").await;
1963        cx.simulate_shared_keystrokes("2 d a w").await;
1964        cx.shared_state().await.assert_matches();
1965
1966        // Trailing whitespace counts as a word unit for iw
1967        cx.set_shared_state("ˇfoo   ").await;
1968        cx.simulate_shared_keystrokes("2 d i w").await;
1969        cx.shared_state().await.assert_matches();
1970
1971        // Multi-line: count > 1 crosses line boundaries
1972        cx.set_shared_state("ˇone\ntwo\nthree").await;
1973        cx.simulate_shared_keystrokes("2 d a w").await;
1974        cx.shared_state().await.assert_matches();
1975
1976        cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
1977        cx.simulate_shared_keystrokes("3 d a w").await;
1978        cx.shared_state().await.assert_matches();
1979
1980        cx.set_shared_state("ˇone\ntwo\nthree").await;
1981        cx.simulate_shared_keystrokes("2 d i w").await;
1982        cx.shared_state().await.assert_matches();
1983
1984        cx.set_shared_state("one ˇtwo\nthree four").await;
1985        cx.simulate_shared_keystrokes("2 d a w").await;
1986        cx.shared_state().await.assert_matches();
1987    }
1988
1989    const PARAGRAPH_EXAMPLES: &[&str] = &[
1990        // Single line
1991        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1992        // Multiple lines without empty lines
1993        indoc! {"
1994            ˇThe quick brownˇ
1995            ˇfox jumps overˇ
1996            the lazy dog.ˇ
1997        "},
1998        // Heading blank paragraph and trailing normal paragraph
1999        indoc! {"
2000            ˇ
2001            ˇ
2002            ˇThe quick brown fox jumps
2003            ˇover the lazy dog.
2004            ˇ
2005            ˇ
2006            ˇThe quick brown fox jumpsˇ
2007            ˇover the lazy dog.ˇ
2008        "},
2009        // Inserted blank paragraph and trailing blank paragraph
2010        indoc! {"
2011            ˇThe quick brown fox jumps
2012            ˇover the lazy dog.
2013            ˇ
2014            ˇ
2015            ˇ
2016            ˇThe quick brown fox jumpsˇ
2017            ˇover the lazy dog.ˇ
2018            ˇ
2019            ˇ
2020            ˇ
2021        "},
2022        // "Blank" paragraph with whitespace characters
2023        indoc! {"
2024            ˇThe quick brown fox jumps
2025            over the lazy dog.
2026
2027            ˇ \t
2028
2029            ˇThe quick brown fox jumps
2030            over the lazy dog.ˇ
2031            ˇ
2032            ˇ \t
2033            \t \t
2034        "},
2035        // Single line "paragraphs", where selection size might be zero.
2036        indoc! {"
2037            ˇThe quick brown fox jumps over the lazy dog.
2038            ˇ
2039            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
2040            ˇ
2041        "},
2042    ];
2043
2044    #[gpui::test]
2045    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
2046        let mut cx = NeovimBackedTestContext::new(cx).await;
2047
2048        for paragraph_example in PARAGRAPH_EXAMPLES {
2049            cx.simulate_at_each_offset("c i p", paragraph_example)
2050                .await
2051                .assert_matches();
2052            cx.simulate_at_each_offset("c a p", paragraph_example)
2053                .await
2054                .assert_matches();
2055        }
2056    }
2057
2058    #[gpui::test]
2059    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
2060        let mut cx = NeovimBackedTestContext::new(cx).await;
2061
2062        for paragraph_example in PARAGRAPH_EXAMPLES {
2063            cx.simulate_at_each_offset("d i p", paragraph_example)
2064                .await
2065                .assert_matches();
2066            cx.simulate_at_each_offset("d a p", paragraph_example)
2067                .await
2068                .assert_matches();
2069        }
2070    }
2071
2072    #[gpui::test]
2073    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
2074        let mut cx = NeovimBackedTestContext::new(cx).await;
2075
2076        const EXAMPLES: &[&str] = &[
2077            indoc! {"
2078                ˇThe quick brown
2079                fox jumps over
2080                the lazy dog.
2081            "},
2082            indoc! {"
2083                ˇ
2084
2085                ˇThe quick brown fox jumps
2086                over the lazy dog.
2087                ˇ
2088
2089                ˇThe quick brown fox jumps
2090                over the lazy dog.
2091            "},
2092            indoc! {"
2093                ˇThe quick brown fox jumps over the lazy dog.
2094                ˇ
2095                ˇThe quick brown fox jumps over the lazy dog.
2096
2097            "},
2098        ];
2099
2100        for paragraph_example in EXAMPLES {
2101            cx.simulate_at_each_offset("v i p", paragraph_example)
2102                .await
2103                .assert_matches();
2104            cx.simulate_at_each_offset("v a p", paragraph_example)
2105                .await
2106                .assert_matches();
2107        }
2108    }
2109
2110    #[gpui::test]
2111    async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2112        let mut cx = NeovimBackedTestContext::new(cx).await;
2113
2114        const WRAPPING_EXAMPLE: &str = indoc! {"
2115            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2116
2117            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2118
2119            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2120        "};
2121
2122        cx.set_shared_wrap(20).await;
2123
2124        cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
2125            .await
2126            .assert_matches();
2127        cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
2128            .await
2129            .assert_matches();
2130    }
2131
2132    #[gpui::test]
2133    async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2134        let mut cx = NeovimBackedTestContext::new(cx).await;
2135
2136        const WRAPPING_EXAMPLE: &str = indoc! {"
2137            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2138
2139            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2140
2141            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2142        "};
2143
2144        cx.set_shared_wrap(20).await;
2145
2146        cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
2147            .await
2148            .assert_matches();
2149        cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
2150            .await
2151            .assert_matches();
2152    }
2153
2154    #[gpui::test]
2155    async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
2156        let mut cx = NeovimBackedTestContext::new(cx).await;
2157
2158        cx.set_shared_state(indoc! {"
2159            a
2160                   ˇ•
2161            aaaaaaaaaaaaa
2162        "})
2163            .await;
2164
2165        cx.simulate_shared_keystrokes("d i p").await;
2166        cx.shared_state().await.assert_eq(indoc! {"
2167            a
2168            aaaaaaaˇaaaaaa
2169        "});
2170    }
2171
2172    #[gpui::test]
2173    async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2174        let mut cx = NeovimBackedTestContext::new(cx).await;
2175
2176        const WRAPPING_EXAMPLE: &str = indoc! {"
2177            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2178
2179            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2180
2181            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2182        "};
2183
2184        cx.set_shared_wrap(20).await;
2185
2186        cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2187            .await
2188            .assert_matches();
2189        cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2190            .await
2191            .assert_matches();
2192    }
2193
2194    // Test string with "`" for opening surrounders and "'" for closing surrounders
2195    const SURROUNDING_MARKER_STRING: &str = indoc! {"
2196        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2197        'ˇfox juˇmps ov`ˇer
2198        the ˇlazy d'o`ˇg"};
2199
2200    const SURROUNDING_OBJECTS: &[(char, char)] = &[
2201        ('"', '"'), // Double Quote
2202        ('(', ')'), // Parentheses
2203    ];
2204
2205    #[gpui::test]
2206    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2207        let mut cx = NeovimBackedTestContext::new(cx).await;
2208
2209        for (start, end) in SURROUNDING_OBJECTS {
2210            let marked_string = SURROUNDING_MARKER_STRING
2211                .replace('`', &start.to_string())
2212                .replace('\'', &end.to_string());
2213
2214            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2215                .await
2216                .assert_matches();
2217            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2218                .await
2219                .assert_matches();
2220            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2221                .await
2222                .assert_matches();
2223            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2224                .await
2225                .assert_matches();
2226        }
2227    }
2228    #[gpui::test]
2229    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2230        let mut cx = NeovimBackedTestContext::new(cx).await;
2231        cx.set_shared_wrap(12).await;
2232
2233        cx.set_shared_state(indoc! {
2234            "\"ˇhello world\"!"
2235        })
2236        .await;
2237        cx.simulate_shared_keystrokes("v i \"").await;
2238        cx.shared_state().await.assert_eq(indoc! {
2239            "\"«hello worldˇ»\"!"
2240        });
2241
2242        cx.set_shared_state(indoc! {
2243            "\"hˇello world\"!"
2244        })
2245        .await;
2246        cx.simulate_shared_keystrokes("v i \"").await;
2247        cx.shared_state().await.assert_eq(indoc! {
2248            "\"«hello worldˇ»\"!"
2249        });
2250
2251        cx.set_shared_state(indoc! {
2252            "helˇlo \"world\"!"
2253        })
2254        .await;
2255        cx.simulate_shared_keystrokes("v i \"").await;
2256        cx.shared_state().await.assert_eq(indoc! {
2257            "hello \"«worldˇ»\"!"
2258        });
2259
2260        cx.set_shared_state(indoc! {
2261            "hello \"wˇorld\"!"
2262        })
2263        .await;
2264        cx.simulate_shared_keystrokes("v i \"").await;
2265        cx.shared_state().await.assert_eq(indoc! {
2266            "hello \"«worldˇ»\"!"
2267        });
2268
2269        cx.set_shared_state(indoc! {
2270            "hello \"wˇorld\"!"
2271        })
2272        .await;
2273        cx.simulate_shared_keystrokes("v a \"").await;
2274        cx.shared_state().await.assert_eq(indoc! {
2275            "hello« \"world\"ˇ»!"
2276        });
2277
2278        cx.set_shared_state(indoc! {
2279            "hello \"wˇorld\" !"
2280        })
2281        .await;
2282        cx.simulate_shared_keystrokes("v a \"").await;
2283        cx.shared_state().await.assert_eq(indoc! {
2284            "hello «\"world\" ˇ»!"
2285        });
2286
2287        cx.set_shared_state(indoc! {
2288            "hello \"wˇorld\"2289            goodbye"
2290        })
2291        .await;
2292        cx.simulate_shared_keystrokes("v a \"").await;
2293        cx.shared_state().await.assert_eq(indoc! {
2294            "hello «\"world\" ˇ»
2295            goodbye"
2296        });
2297    }
2298
2299    #[gpui::test]
2300    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2301        let mut cx = VimTestContext::new(cx, true).await;
2302
2303        cx.set_state(
2304            indoc! {
2305                "func empty(a string) bool {
2306                   if a == \"\" {
2307                      return true
2308                   }
2309                   ˇreturn false
2310                }"
2311            },
2312            Mode::Normal,
2313        );
2314        cx.simulate_keystrokes("v i {");
2315        cx.assert_state(
2316            indoc! {
2317                "func empty(a string) bool {
2318                   «if a == \"\" {
2319                      return true
2320                   }
2321                   return falseˇ»
2322                }"
2323            },
2324            Mode::Visual,
2325        );
2326
2327        cx.set_state(
2328            indoc! {
2329                "func empty(a string) bool {
2330                     if a == \"\" {
2331                         ˇreturn true
2332                     }
2333                     return false
2334                }"
2335            },
2336            Mode::Normal,
2337        );
2338        cx.simulate_keystrokes("v i {");
2339        cx.assert_state(
2340            indoc! {
2341                "func empty(a string) bool {
2342                     if a == \"\" {
2343                         «return trueˇ»
2344                     }
2345                     return false
2346                }"
2347            },
2348            Mode::Visual,
2349        );
2350
2351        cx.set_state(
2352            indoc! {
2353                "func empty(a string) bool {
2354                     if a == \"\" ˇ{
2355                         return true
2356                     }
2357                     return false
2358                }"
2359            },
2360            Mode::Normal,
2361        );
2362        cx.simulate_keystrokes("v i {");
2363        cx.assert_state(
2364            indoc! {
2365                "func empty(a string) bool {
2366                     if a == \"\" {
2367                         «return trueˇ»
2368                     }
2369                     return false
2370                }"
2371            },
2372            Mode::Visual,
2373        );
2374
2375        cx.set_state(
2376            indoc! {
2377                "func empty(a string) bool {
2378                     if a == \"\" {
2379                         return true
2380                     }
2381                     return false
2382                ˇ}"
2383            },
2384            Mode::Normal,
2385        );
2386        cx.simulate_keystrokes("v i {");
2387        cx.assert_state(
2388            indoc! {
2389                "func empty(a string) bool {
2390                     «if a == \"\" {
2391                         return true
2392                     }
2393                     return falseˇ»
2394                }"
2395            },
2396            Mode::Visual,
2397        );
2398
2399        cx.set_state(
2400            indoc! {
2401                "func empty(a string) bool {
2402                             if a == \"\" {
2403                             ˇ
2404
2405                             }"
2406            },
2407            Mode::Normal,
2408        );
2409        cx.simulate_keystrokes("c i {");
2410        cx.assert_state(
2411            indoc! {
2412                "func empty(a string) bool {
2413                             if a == \"\" {ˇ}"
2414            },
2415            Mode::Insert,
2416        );
2417    }
2418
2419    #[gpui::test]
2420    async fn test_singleline_surrounding_character_objects_with_escape(
2421        cx: &mut gpui::TestAppContext,
2422    ) {
2423        let mut cx = NeovimBackedTestContext::new(cx).await;
2424        cx.set_shared_state(indoc! {
2425            "h\"e\\\"lˇlo \\\"world\"!"
2426        })
2427        .await;
2428        cx.simulate_shared_keystrokes("v i \"").await;
2429        cx.shared_state().await.assert_eq(indoc! {
2430            "h\"«e\\\"llo \\\"worldˇ»\"!"
2431        });
2432
2433        cx.set_shared_state(indoc! {
2434            "hello \"teˇst \\\"inside\\\" world\""
2435        })
2436        .await;
2437        cx.simulate_shared_keystrokes("v i \"").await;
2438        cx.shared_state().await.assert_eq(indoc! {
2439            "hello \"«test \\\"inside\\\" worldˇ»\""
2440        });
2441    }
2442
2443    #[gpui::test]
2444    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2445        let mut cx = VimTestContext::new(cx, true).await;
2446        cx.set_state(
2447            indoc! {"
2448            fn boop() {
2449                baz(ˇ|a, b| { bar(|j, k| { })})
2450            }"
2451            },
2452            Mode::Normal,
2453        );
2454        cx.simulate_keystrokes("c i |");
2455        cx.assert_state(
2456            indoc! {"
2457            fn boop() {
2458                baz(|ˇ| { bar(|j, k| { })})
2459            }"
2460            },
2461            Mode::Insert,
2462        );
2463        cx.simulate_keystrokes("escape 1 8 |");
2464        cx.assert_state(
2465            indoc! {"
2466            fn boop() {
2467                baz(|| { bar(ˇ|j, k| { })})
2468            }"
2469            },
2470            Mode::Normal,
2471        );
2472
2473        cx.simulate_keystrokes("v a |");
2474        cx.assert_state(
2475            indoc! {"
2476            fn boop() {
2477                baz(|| { bar(«|j, k| ˇ»{ })})
2478            }"
2479            },
2480            Mode::Visual,
2481        );
2482    }
2483
2484    #[gpui::test]
2485    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2486        let mut cx = VimTestContext::new(cx, true).await;
2487
2488        // Generic arguments
2489        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2490        cx.simulate_keystrokes("v i a");
2491        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2492
2493        // Function arguments
2494        cx.set_state(
2495            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2496            Mode::Normal,
2497        );
2498        cx.simulate_keystrokes("d a a");
2499        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2500
2501        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2502        cx.simulate_keystrokes("v a a");
2503        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2504
2505        // Tuple, vec, and array arguments
2506        cx.set_state(
2507            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2508            Mode::Normal,
2509        );
2510        cx.simulate_keystrokes("c i a");
2511        cx.assert_state(
2512            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2513            Mode::Insert,
2514        );
2515
2516        // TODO regressed with the up-to-date Rust grammar.
2517        // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2518        // cx.simulate_keystrokes("c a a");
2519        // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2520
2521        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2522        cx.simulate_keystrokes("c i a");
2523        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2524
2525        cx.set_state(
2526            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2527            Mode::Normal,
2528        );
2529        cx.simulate_keystrokes("c a a");
2530        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2531
2532        // Cursor immediately before / after brackets
2533        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2534        cx.simulate_keystrokes("v i a");
2535        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2536
2537        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2538        cx.simulate_keystrokes("v i a");
2539        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2540    }
2541
2542    #[gpui::test]
2543    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2544        let mut cx = VimTestContext::new(cx, true).await;
2545
2546        // Base use case
2547        cx.set_state(
2548            indoc! {"
2549                fn boop() {
2550                    // Comment
2551                    baz();ˇ
2552
2553                    loop {
2554                        bar(1);
2555                        bar(2);
2556                    }
2557
2558                    result
2559                }
2560            "},
2561            Mode::Normal,
2562        );
2563        cx.simulate_keystrokes("v i i");
2564        cx.assert_state(
2565            indoc! {"
2566                fn boop() {
2567                «    // Comment
2568                    baz();
2569
2570                    loop {
2571                        bar(1);
2572                        bar(2);
2573                    }
2574
2575                    resultˇ»
2576                }
2577            "},
2578            Mode::Visual,
2579        );
2580
2581        // Around indent (include line above)
2582        cx.set_state(
2583            indoc! {"
2584                const ABOVE: str = true;
2585                fn boop() {
2586
2587                    hello();
2588                    worˇld()
2589                }
2590            "},
2591            Mode::Normal,
2592        );
2593        cx.simulate_keystrokes("v a i");
2594        cx.assert_state(
2595            indoc! {"
2596                const ABOVE: str = true;
2597                «fn boop() {
2598
2599                    hello();
2600                    world()ˇ»
2601                }
2602            "},
2603            Mode::Visual,
2604        );
2605
2606        // Around indent (include line above & below)
2607        cx.set_state(
2608            indoc! {"
2609                const ABOVE: str = true;
2610                fn boop() {
2611                    hellˇo();
2612                    world()
2613
2614                }
2615                const BELOW: str = true;
2616            "},
2617            Mode::Normal,
2618        );
2619        cx.simulate_keystrokes("c a shift-i");
2620        cx.assert_state(
2621            indoc! {"
2622                const ABOVE: str = true;
2623                ˇ
2624                const BELOW: str = true;
2625            "},
2626            Mode::Insert,
2627        );
2628    }
2629
2630    #[gpui::test]
2631    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2632        let mut cx = NeovimBackedTestContext::new(cx).await;
2633
2634        for (start, end) in SURROUNDING_OBJECTS {
2635            let marked_string = SURROUNDING_MARKER_STRING
2636                .replace('`', &start.to_string())
2637                .replace('\'', &end.to_string());
2638
2639            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2640                .await
2641                .assert_matches();
2642            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2643                .await
2644                .assert_matches();
2645            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2646                .await
2647                .assert_matches();
2648            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2649                .await
2650                .assert_matches();
2651        }
2652    }
2653
2654    #[gpui::test]
2655    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2656        let mut cx = VimTestContext::new(cx, true).await;
2657        cx.update(|_, cx| {
2658            cx.bind_keys([KeyBinding::new(
2659                "q",
2660                AnyQuotes,
2661                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2662            )]);
2663        });
2664
2665        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2666            // the false string in the middle should be considered
2667            (
2668                "c i q",
2669                "'first' false ˇstring 'second'",
2670                "'first'ˇ'second'",
2671                Mode::Insert,
2672            ),
2673            // Single quotes
2674            (
2675                "c i q",
2676                "Thisˇ is a 'quote' example.",
2677                "This is a 'ˇ' example.",
2678                Mode::Insert,
2679            ),
2680            (
2681                "c a q",
2682                "Thisˇ is a 'quote' example.",
2683                "This is a ˇexample.",
2684                Mode::Insert,
2685            ),
2686            (
2687                "c i q",
2688                "This is a \"simple 'qˇuote'\" example.",
2689                "This is a \"simple 'ˇ'\" example.",
2690                Mode::Insert,
2691            ),
2692            (
2693                "c a q",
2694                "This is a \"simple 'qˇuote'\" example.",
2695                "This is a \"simpleˇ\" example.",
2696                Mode::Insert,
2697            ),
2698            (
2699                "c i q",
2700                "This is a 'qˇuote' example.",
2701                "This is a 'ˇ' example.",
2702                Mode::Insert,
2703            ),
2704            (
2705                "c a q",
2706                "This is a 'qˇuote' example.",
2707                "This is a ˇexample.",
2708                Mode::Insert,
2709            ),
2710            (
2711                "d i q",
2712                "This is a 'qˇuote' example.",
2713                "This is a 'ˇ' example.",
2714                Mode::Normal,
2715            ),
2716            (
2717                "d a q",
2718                "This is a 'qˇuote' example.",
2719                "This is a ˇexample.",
2720                Mode::Normal,
2721            ),
2722            // Double quotes
2723            (
2724                "c i q",
2725                "This is a \"qˇuote\" example.",
2726                "This is a \"ˇ\" example.",
2727                Mode::Insert,
2728            ),
2729            (
2730                "c a q",
2731                "This is a \"qˇuote\" example.",
2732                "This is a ˇexample.",
2733                Mode::Insert,
2734            ),
2735            (
2736                "d i q",
2737                "This is a \"qˇuote\" example.",
2738                "This is a \"ˇ\" example.",
2739                Mode::Normal,
2740            ),
2741            (
2742                "d a q",
2743                "This is a \"qˇuote\" example.",
2744                "This is a ˇexample.",
2745                Mode::Normal,
2746            ),
2747            // Back quotes
2748            (
2749                "c i q",
2750                "This is a `qˇuote` example.",
2751                "This is a `ˇ` example.",
2752                Mode::Insert,
2753            ),
2754            (
2755                "c a q",
2756                "This is a `qˇuote` example.",
2757                "This is a ˇexample.",
2758                Mode::Insert,
2759            ),
2760            (
2761                "d i q",
2762                "This is a `qˇuote` example.",
2763                "This is a `ˇ` example.",
2764                Mode::Normal,
2765            ),
2766            (
2767                "d a q",
2768                "This is a `qˇuote` example.",
2769                "This is a ˇexample.",
2770                Mode::Normal,
2771            ),
2772        ];
2773
2774        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2775            cx.set_state(initial_state, Mode::Normal);
2776
2777            cx.simulate_keystrokes(keystrokes);
2778
2779            cx.assert_state(expected_state, *expected_mode);
2780        }
2781
2782        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2783            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2784            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2785            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2786            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2787            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2788            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2789            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2790            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2791            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2792            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2793            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2794            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2795        ];
2796
2797        for (keystrokes, initial_state, mode) in INVALID_CASES {
2798            cx.set_state(initial_state, Mode::Normal);
2799
2800            cx.simulate_keystrokes(keystrokes);
2801
2802            cx.assert_state(initial_state, *mode);
2803        }
2804    }
2805
2806    #[gpui::test]
2807    async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2808        let mut cx = VimTestContext::new_typescript(cx).await;
2809
2810        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2811            // Special cases from mini.ai plugin
2812            // the false string in the middle should not be considered
2813            (
2814                "c i q",
2815                "'first' false ˇstring 'second'",
2816                "'first' false string 'ˇ'",
2817                Mode::Insert,
2818            ),
2819            // Multiline support :)! Same behavior as mini.ai plugin
2820            (
2821                "c i q",
2822                indoc! {"
2823                    `
2824                    first
2825                    middle ˇstring
2826                    second
2827                    `
2828                "},
2829                indoc! {"
2830                    `ˇ`
2831                "},
2832                Mode::Insert,
2833            ),
2834            // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2835            // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2836            // Bug reference: https://github.com/zed-industries/zed/issues/23889
2837            ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2838            // Single quotes
2839            (
2840                "c i q",
2841                "Thisˇ is a 'quote' example.",
2842                "This is a 'ˇ' example.",
2843                Mode::Insert,
2844            ),
2845            (
2846                "c a q",
2847                "Thisˇ is a 'quote' example.",
2848                "This is a ˇ example.", // same mini.ai plugin behavior
2849                Mode::Insert,
2850            ),
2851            (
2852                "c i q",
2853                "This is a \"simple 'qˇuote'\" example.",
2854                "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2855                Mode::Insert,
2856            ),
2857            (
2858                "c a q",
2859                "This is a \"simple 'qˇuote'\" example.",
2860                "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2861                Mode::Insert,
2862            ),
2863            (
2864                "c i q",
2865                "This is a 'qˇuote' example.",
2866                "This is a 'ˇ' example.",
2867                Mode::Insert,
2868            ),
2869            (
2870                "c a q",
2871                "This is a 'qˇuote' example.",
2872                "This is a ˇ example.", // same mini.ai plugin behavior
2873                Mode::Insert,
2874            ),
2875            (
2876                "d i q",
2877                "This is a 'qˇuote' example.",
2878                "This is a 'ˇ' example.",
2879                Mode::Normal,
2880            ),
2881            (
2882                "d a q",
2883                "This is a 'qˇuote' example.",
2884                "This is a ˇ example.", // same mini.ai plugin behavior
2885                Mode::Normal,
2886            ),
2887            // Double quotes
2888            (
2889                "c i q",
2890                "This is a \"qˇuote\" example.",
2891                "This is a \"ˇ\" example.",
2892                Mode::Insert,
2893            ),
2894            (
2895                "c a q",
2896                "This is a \"qˇuote\" example.",
2897                "This is a ˇ example.", // same mini.ai plugin behavior
2898                Mode::Insert,
2899            ),
2900            (
2901                "d i q",
2902                "This is a \"qˇuote\" example.",
2903                "This is a \"ˇ\" example.",
2904                Mode::Normal,
2905            ),
2906            (
2907                "d a q",
2908                "This is a \"qˇuote\" example.",
2909                "This is a ˇ example.", // same mini.ai plugin behavior
2910                Mode::Normal,
2911            ),
2912            // Back quotes
2913            (
2914                "c i q",
2915                "This is a `qˇuote` example.",
2916                "This is a `ˇ` example.",
2917                Mode::Insert,
2918            ),
2919            (
2920                "c a q",
2921                "This is a `qˇuote` example.",
2922                "This is a ˇ example.", // same mini.ai plugin behavior
2923                Mode::Insert,
2924            ),
2925            (
2926                "d i q",
2927                "This is a `qˇuote` example.",
2928                "This is a `ˇ` example.",
2929                Mode::Normal,
2930            ),
2931            (
2932                "d a q",
2933                "This is a `qˇuote` example.",
2934                "This is a ˇ example.", // same mini.ai plugin behavior
2935                Mode::Normal,
2936            ),
2937        ];
2938
2939        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2940            cx.set_state(initial_state, Mode::Normal);
2941            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2942            cx.simulate_keystrokes(keystrokes);
2943            cx.assert_state(expected_state, *expected_mode);
2944        }
2945
2946        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2947            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2948            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2949            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2950            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2951            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2952            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2953            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2954            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2955            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2956            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2957            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2958            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2959        ];
2960
2961        for (keystrokes, initial_state, mode) in INVALID_CASES {
2962            cx.set_state(initial_state, Mode::Normal);
2963            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2964            cx.simulate_keystrokes(keystrokes);
2965            cx.assert_state(initial_state, *mode);
2966        }
2967    }
2968
2969    #[gpui::test]
2970    async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2971        let mut cx = VimTestContext::new(cx, true).await;
2972        cx.update(|_, cx| {
2973            cx.bind_keys([KeyBinding::new(
2974                "b",
2975                AnyBrackets,
2976                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2977            )]);
2978        });
2979
2980        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2981            (
2982                "c i b",
2983                indoc! {"
2984                    {
2985                        {
2986                            ˇprint('hello')
2987                        }
2988                    }
2989                "},
2990                indoc! {"
2991                    {
2992                        {
2993                            ˇ
2994                        }
2995                    }
2996                "},
2997                Mode::Insert,
2998            ),
2999            // Bracket (Parentheses)
3000            (
3001                "c i b",
3002                "Thisˇ is a (simple [quote]) example.",
3003                "This is a (ˇ) example.",
3004                Mode::Insert,
3005            ),
3006            (
3007                "c i b",
3008                "This is a [simple (qˇuote)] example.",
3009                "This is a [simple (ˇ)] example.",
3010                Mode::Insert,
3011            ),
3012            (
3013                "c a b",
3014                "This is a [simple (qˇuote)] example.",
3015                "This is a [simple ˇ] example.",
3016                Mode::Insert,
3017            ),
3018            (
3019                "c a b",
3020                "Thisˇ is a (simple [quote]) example.",
3021                "This is a ˇ example.",
3022                Mode::Insert,
3023            ),
3024            (
3025                "c i b",
3026                "This is a (qˇuote) example.",
3027                "This is a (ˇ) example.",
3028                Mode::Insert,
3029            ),
3030            (
3031                "c a b",
3032                "This is a (qˇuote) example.",
3033                "This is a ˇ example.",
3034                Mode::Insert,
3035            ),
3036            (
3037                "d i b",
3038                "This is a (qˇuote) example.",
3039                "This is a (ˇ) example.",
3040                Mode::Normal,
3041            ),
3042            (
3043                "d a b",
3044                "This is a (qˇuote) example.",
3045                "This is a ˇ example.",
3046                Mode::Normal,
3047            ),
3048            // Square brackets
3049            (
3050                "c i b",
3051                "This is a [qˇuote] example.",
3052                "This is a [ˇ] example.",
3053                Mode::Insert,
3054            ),
3055            (
3056                "c a b",
3057                "This is a [qˇuote] example.",
3058                "This is a ˇ example.",
3059                Mode::Insert,
3060            ),
3061            (
3062                "d i b",
3063                "This is a [qˇuote] example.",
3064                "This is a [ˇ] example.",
3065                Mode::Normal,
3066            ),
3067            (
3068                "d a b",
3069                "This is a [qˇuote] example.",
3070                "This is a ˇ example.",
3071                Mode::Normal,
3072            ),
3073            // Curly brackets
3074            (
3075                "c i b",
3076                "This is a {qˇuote} example.",
3077                "This is a {ˇ} example.",
3078                Mode::Insert,
3079            ),
3080            (
3081                "c a b",
3082                "This is a {qˇuote} example.",
3083                "This is a ˇ example.",
3084                Mode::Insert,
3085            ),
3086            (
3087                "d i b",
3088                "This is a {qˇuote} example.",
3089                "This is a {ˇ} example.",
3090                Mode::Normal,
3091            ),
3092            (
3093                "d a b",
3094                "This is a {qˇuote} example.",
3095                "This is a ˇ example.",
3096                Mode::Normal,
3097            ),
3098        ];
3099
3100        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3101            cx.set_state(initial_state, Mode::Normal);
3102
3103            cx.simulate_keystrokes(keystrokes);
3104
3105            cx.assert_state(expected_state, *expected_mode);
3106        }
3107
3108        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3109            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3110            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3111            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3112            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3113            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3114            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3115            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3116            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3117            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3118            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3119            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3120            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3121        ];
3122
3123        for (keystrokes, initial_state, mode) in INVALID_CASES {
3124            cx.set_state(initial_state, Mode::Normal);
3125
3126            cx.simulate_keystrokes(keystrokes);
3127
3128            cx.assert_state(initial_state, *mode);
3129        }
3130    }
3131
3132    #[gpui::test]
3133    async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3134        let mut cx = VimTestContext::new(cx, true).await;
3135        cx.update(|_, cx| {
3136            cx.bind_keys([KeyBinding::new(
3137                "b",
3138                MiniBrackets,
3139                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3140            )]);
3141        });
3142
3143        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3144            // Special cases from mini.ai plugin
3145            // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3146            // Same behavior as mini.ai plugin
3147            (
3148                "c i b",
3149                indoc! {"
3150                    {
3151                        {
3152                            ˇprint('hello')
3153                        }
3154                    }
3155                "},
3156                indoc! {"
3157                    {
3158                        {
3159                            print(ˇ)
3160                        }
3161                    }
3162                "},
3163                Mode::Insert,
3164            ),
3165            // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3166            // Same behavior as mini.ai plugin
3167            (
3168                "c i b",
3169                indoc! {"
3170                    {
3171                        {
3172                            ˇ
3173                            print('hello')
3174                        }
3175                    }
3176                "},
3177                indoc! {"
3178                    {
3179                        {ˇ}
3180                    }
3181                "},
3182                Mode::Insert,
3183            ),
3184            // If you are in the open bracket then it has higher priority
3185            (
3186                "c i b",
3187                indoc! {"
3188                    «{ˇ»
3189                        {
3190                            print('hello')
3191                        }
3192                    }
3193                "},
3194                indoc! {"
3195                    {ˇ}
3196                "},
3197                Mode::Insert,
3198            ),
3199            // If you are in the close bracket then it has higher priority
3200            (
3201                "c i b",
3202                indoc! {"
3203                    {
3204                        {
3205                            print('hello')
3206                        }
3207                    «}ˇ»
3208                "},
3209                indoc! {"
3210                    {ˇ}
3211                "},
3212                Mode::Insert,
3213            ),
3214            // Bracket (Parentheses)
3215            (
3216                "c i b",
3217                "Thisˇ is a (simple [quote]) example.",
3218                "This is a (ˇ) example.",
3219                Mode::Insert,
3220            ),
3221            (
3222                "c i b",
3223                "This is a [simple (qˇuote)] example.",
3224                "This is a [simple (ˇ)] example.",
3225                Mode::Insert,
3226            ),
3227            (
3228                "c a b",
3229                "This is a [simple (qˇuote)] example.",
3230                "This is a [simple ˇ] example.",
3231                Mode::Insert,
3232            ),
3233            (
3234                "c a b",
3235                "Thisˇ is a (simple [quote]) example.",
3236                "This is a ˇ example.",
3237                Mode::Insert,
3238            ),
3239            (
3240                "c i b",
3241                "This is a (qˇuote) example.",
3242                "This is a (ˇ) example.",
3243                Mode::Insert,
3244            ),
3245            (
3246                "c a b",
3247                "This is a (qˇuote) example.",
3248                "This is a ˇ example.",
3249                Mode::Insert,
3250            ),
3251            (
3252                "d i b",
3253                "This is a (qˇuote) example.",
3254                "This is a (ˇ) example.",
3255                Mode::Normal,
3256            ),
3257            (
3258                "d a b",
3259                "This is a (qˇuote) example.",
3260                "This is a ˇ example.",
3261                Mode::Normal,
3262            ),
3263            // Square brackets
3264            (
3265                "c i b",
3266                "This is a [qˇuote] example.",
3267                "This is a [ˇ] example.",
3268                Mode::Insert,
3269            ),
3270            (
3271                "c a b",
3272                "This is a [qˇuote] example.",
3273                "This is a ˇ example.",
3274                Mode::Insert,
3275            ),
3276            (
3277                "d i b",
3278                "This is a [qˇuote] example.",
3279                "This is a [ˇ] example.",
3280                Mode::Normal,
3281            ),
3282            (
3283                "d a b",
3284                "This is a [qˇuote] example.",
3285                "This is a ˇ example.",
3286                Mode::Normal,
3287            ),
3288            // Curly brackets
3289            (
3290                "c i b",
3291                "This is a {qˇuote} example.",
3292                "This is a {ˇ} example.",
3293                Mode::Insert,
3294            ),
3295            (
3296                "c a b",
3297                "This is a {qˇuote} example.",
3298                "This is a ˇ example.",
3299                Mode::Insert,
3300            ),
3301            (
3302                "d i b",
3303                "This is a {qˇuote} example.",
3304                "This is a {ˇ} example.",
3305                Mode::Normal,
3306            ),
3307            (
3308                "d a b",
3309                "This is a {qˇuote} example.",
3310                "This is a ˇ example.",
3311                Mode::Normal,
3312            ),
3313        ];
3314
3315        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3316            cx.set_state(initial_state, Mode::Normal);
3317            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3318            cx.simulate_keystrokes(keystrokes);
3319            cx.assert_state(expected_state, *expected_mode);
3320        }
3321
3322        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3323            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3324            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3325            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3326            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3327            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3328            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3329            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3330            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3331            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3332            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3333            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3334            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3335        ];
3336
3337        for (keystrokes, initial_state, mode) in INVALID_CASES {
3338            cx.set_state(initial_state, Mode::Normal);
3339            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3340            cx.simulate_keystrokes(keystrokes);
3341            cx.assert_state(initial_state, *mode);
3342        }
3343    }
3344
3345    #[gpui::test]
3346    async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3347        // Initialize test context with the TypeScript language loaded, so we
3348        // can actually get brackets definition.
3349        let mut cx = VimTestContext::new(cx, true).await;
3350
3351        // Update `b` to `MiniBrackets` so we can later use it when simulating
3352        // keystrokes.
3353        cx.update(|_, cx| {
3354            cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3355        });
3356
3357        let (editor, cx) = cx.add_window_view(|window, cx| {
3358            let multi_buffer = MultiBuffer::build_multi(
3359                [
3360                    ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3361                    ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3362                ],
3363                cx,
3364            );
3365
3366            // In order for the brackets to actually be found, we need to update
3367            // the language used for the second buffer. This is something that
3368            // is handled automatically when simply using `VimTestContext::new`
3369            // but, since this is being set manually, the language isn't
3370            // automatically set.
3371            let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3372            let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
3373            if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3374                buffer.update(cx, |buffer, cx| {
3375                    buffer.set_language(Some(language::rust_lang()), cx);
3376                })
3377            };
3378
3379            editor
3380        });
3381
3382        let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3383
3384        cx.assert_excerpts_with_selections(indoc! {"
3385            [EXCERPT]
3386            ˇ111
3387            222
3388            [EXCERPT]
3389            111
3390            a {bracket} example
3391            "
3392        });
3393
3394        cx.simulate_keystrokes("j j j j f r");
3395        cx.assert_excerpts_with_selections(indoc! {"
3396            [EXCERPT]
3397            111
3398            222
3399            [EXCERPT]
3400            111
3401            a {bˇracket} example
3402            "
3403        });
3404
3405        cx.simulate_keystrokes("d i b");
3406        cx.assert_excerpts_with_selections(indoc! {"
3407            [EXCERPT]
3408            111
3409            222
3410            [EXCERPT]
3411            111
3412            a {ˇ} example
3413            "
3414        });
3415    }
3416
3417    #[gpui::test]
3418    async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3419        let mut cx = NeovimBackedTestContext::new(cx).await;
3420        cx.set_shared_state("(trailingˇ whitespace          )")
3421            .await;
3422        cx.simulate_shared_keystrokes("v i b").await;
3423        cx.shared_state().await.assert_matches();
3424        cx.simulate_shared_keystrokes("escape y i b").await;
3425        cx.shared_clipboard()
3426            .await
3427            .assert_eq("trailing whitespace          ");
3428    }
3429
3430    #[gpui::test]
3431    async fn test_tags(cx: &mut gpui::TestAppContext) {
3432        let mut cx = VimTestContext::new_html(cx).await;
3433
3434        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3435        cx.simulate_keystrokes("v i t");
3436        cx.assert_state(
3437            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3438            Mode::Visual,
3439        );
3440        cx.simulate_keystrokes("a t");
3441        cx.assert_state(
3442            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3443            Mode::Visual,
3444        );
3445        cx.simulate_keystrokes("a t");
3446        cx.assert_state(
3447            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3448            Mode::Visual,
3449        );
3450
3451        // The cursor is before the tag
3452        cx.set_state(
3453            "<html><head></head><body> ˇ  <b>hi!</b></body>",
3454            Mode::Normal,
3455        );
3456        cx.simulate_keystrokes("v i t");
3457        cx.assert_state(
3458            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
3459            Mode::Visual,
3460        );
3461        cx.simulate_keystrokes("a t");
3462        cx.assert_state(
3463            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
3464            Mode::Visual,
3465        );
3466
3467        // The cursor is in the open tag
3468        cx.set_state(
3469            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3470            Mode::Normal,
3471        );
3472        cx.simulate_keystrokes("v a t");
3473        cx.assert_state(
3474            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3475            Mode::Visual,
3476        );
3477        cx.simulate_keystrokes("i t");
3478        cx.assert_state(
3479            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3480            Mode::Visual,
3481        );
3482
3483        // current selection length greater than 1
3484        cx.set_state(
3485            "<html><head></head><body><«b>hi!ˇ»</b></body>",
3486            Mode::Visual,
3487        );
3488        cx.simulate_keystrokes("i t");
3489        cx.assert_state(
3490            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3491            Mode::Visual,
3492        );
3493        cx.simulate_keystrokes("a t");
3494        cx.assert_state(
3495            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3496            Mode::Visual,
3497        );
3498
3499        cx.set_state(
3500            "<html><head></head><body><«b>hi!</ˇ»b></body>",
3501            Mode::Visual,
3502        );
3503        cx.simulate_keystrokes("a t");
3504        cx.assert_state(
3505            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3506            Mode::Visual,
3507        );
3508    }
3509    #[gpui::test]
3510    async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3511        let mut cx = NeovimBackedTestContext::new(cx).await;
3512
3513        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3514            .await;
3515        cx.simulate_shared_keystrokes("v a w").await;
3516        cx.shared_state()
3517            .await
3518            .assert_eq("    «const ˇ»f = (x: unknown) => {");
3519
3520        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3521            .await;
3522        cx.simulate_shared_keystrokes("y a w").await;
3523        cx.shared_clipboard().await.assert_eq("const ");
3524
3525        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3526            .await;
3527        cx.simulate_shared_keystrokes("d a w").await;
3528        cx.shared_state()
3529            .await
3530            .assert_eq("    ˇf = (x: unknown) => {");
3531        cx.shared_clipboard().await.assert_eq("const ");
3532
3533        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3534            .await;
3535        cx.simulate_shared_keystrokes("c a w").await;
3536        cx.shared_state()
3537            .await
3538            .assert_eq("    ˇf = (x: unknown) => {");
3539        cx.shared_clipboard().await.assert_eq("const ");
3540    }
3541
3542    #[gpui::test]
3543    async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
3544        let mut cx = VimTestContext::new_typescript(cx).await;
3545
3546        cx.set_state(
3547            indoc! {"
3548                const foo = () => {
3549                    return ˇ1;
3550                };
3551            "},
3552            Mode::Normal,
3553        );
3554        cx.simulate_keystrokes("v a f");
3555        cx.assert_state(
3556            indoc! {"
3557                «const foo = () => {
3558                    return 1;
3559                };ˇ»
3560            "},
3561            Mode::VisualLine,
3562        );
3563
3564        cx.set_state(
3565            indoc! {"
3566                arr.map(() => {
3567                    return ˇ1;
3568                });
3569            "},
3570            Mode::Normal,
3571        );
3572        cx.simulate_keystrokes("v a f");
3573        cx.assert_state(
3574            indoc! {"
3575                arr.map(«() => {
3576                    return 1;
3577                }ˇ»);
3578            "},
3579            Mode::VisualLine,
3580        );
3581
3582        cx.set_state(
3583            indoc! {"
3584                const foo = () => {
3585                    return ˇ1;
3586                };
3587            "},
3588            Mode::Normal,
3589        );
3590        cx.simulate_keystrokes("v i f");
3591        cx.assert_state(
3592            indoc! {"
3593                const foo = () => {
3594                    «return 1;ˇ»
3595                };
3596            "},
3597            Mode::Visual,
3598        );
3599
3600        cx.set_state(
3601            indoc! {"
3602                (() => {
3603                    console.log(ˇ1);
3604                })();
3605            "},
3606            Mode::Normal,
3607        );
3608        cx.simulate_keystrokes("v a f");
3609        cx.assert_state(
3610            indoc! {"
3611                («() => {
3612                    console.log(1);
3613                }ˇ»)();
3614            "},
3615            Mode::VisualLine,
3616        );
3617
3618        cx.set_state(
3619            indoc! {"
3620                const foo = () => {
3621                    return ˇ1;
3622                };
3623                export { foo };
3624            "},
3625            Mode::Normal,
3626        );
3627        cx.simulate_keystrokes("v a f");
3628        cx.assert_state(
3629            indoc! {"
3630                «const foo = () => {
3631                    return 1;
3632                };ˇ»
3633                export { foo };
3634            "},
3635            Mode::VisualLine,
3636        );
3637
3638        cx.set_state(
3639            indoc! {"
3640                let bar = () => {
3641                    return ˇ2;
3642                };
3643            "},
3644            Mode::Normal,
3645        );
3646        cx.simulate_keystrokes("v a f");
3647        cx.assert_state(
3648            indoc! {"
3649                «let bar = () => {
3650                    return 2;
3651                };ˇ»
3652            "},
3653            Mode::VisualLine,
3654        );
3655
3656        cx.set_state(
3657            indoc! {"
3658                var baz = () => {
3659                    return ˇ3;
3660                };
3661            "},
3662            Mode::Normal,
3663        );
3664        cx.simulate_keystrokes("v a f");
3665        cx.assert_state(
3666            indoc! {"
3667                «var baz = () => {
3668                    return 3;
3669                };ˇ»
3670            "},
3671            Mode::VisualLine,
3672        );
3673
3674        cx.set_state(
3675            indoc! {"
3676                const add = (a, b) => a + ˇb;
3677            "},
3678            Mode::Normal,
3679        );
3680        cx.simulate_keystrokes("v a f");
3681        cx.assert_state(
3682            indoc! {"
3683                «const add = (a, b) => a + b;ˇ»
3684            "},
3685            Mode::VisualLine,
3686        );
3687
3688        cx.set_state(
3689            indoc! {"
3690                const add = ˇ(a, b) => a + b;
3691            "},
3692            Mode::Normal,
3693        );
3694        cx.simulate_keystrokes("v a f");
3695        cx.assert_state(
3696            indoc! {"
3697                «const add = (a, b) => a + b;ˇ»
3698            "},
3699            Mode::VisualLine,
3700        );
3701
3702        cx.set_state(
3703            indoc! {"
3704                const add = (a, b) => a + bˇ;
3705            "},
3706            Mode::Normal,
3707        );
3708        cx.simulate_keystrokes("v a f");
3709        cx.assert_state(
3710            indoc! {"
3711                «const add = (a, b) => a + b;ˇ»
3712            "},
3713            Mode::VisualLine,
3714        );
3715
3716        cx.set_state(
3717            indoc! {"
3718                const add = (a, b) =ˇ> a + b;
3719            "},
3720            Mode::Normal,
3721        );
3722        cx.simulate_keystrokes("v a f");
3723        cx.assert_state(
3724            indoc! {"
3725                «const add = (a, b) => a + b;ˇ»
3726            "},
3727            Mode::VisualLine,
3728        );
3729    }
3730
3731    #[gpui::test]
3732    async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
3733        let mut cx = VimTestContext::new_tsx(cx).await;
3734
3735        cx.set_state(
3736            indoc! {r#"
3737                export const MyComponent = () => {
3738                  return (
3739                    <div>
3740                      <div onClick={() => {
3741                        alert("Hello world!");
3742                        console.log(ˇ"clicked");
3743                      }}>Hello world!</div>
3744                    </div>
3745                  );
3746                };
3747            "#},
3748            Mode::Normal,
3749        );
3750        cx.simulate_keystrokes("v a f");
3751        cx.assert_state(
3752            indoc! {r#"
3753                export const MyComponent = () => {
3754                  return (
3755                    <div>
3756                      <div onClick={«() => {
3757                        alert("Hello world!");
3758                        console.log("clicked");
3759                      }ˇ»}>Hello world!</div>
3760                    </div>
3761                  );
3762                };
3763            "#},
3764            Mode::VisualLine,
3765        );
3766
3767        cx.set_state(
3768            indoc! {r#"
3769                export const MyComponent = () => {
3770                  return (
3771                    <div>
3772                      <div onClick={() => console.log("clickˇed")}>Hello world!</div>
3773                    </div>
3774                  );
3775                };
3776            "#},
3777            Mode::Normal,
3778        );
3779        cx.simulate_keystrokes("v a f");
3780        cx.assert_state(
3781            indoc! {r#"
3782                export const MyComponent = () => {
3783                  return (
3784                    <div>
3785                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3786                    </div>
3787                  );
3788                };
3789            "#},
3790            Mode::VisualLine,
3791        );
3792
3793        cx.set_state(
3794            indoc! {r#"
3795                export const MyComponent = () => {
3796                  return (
3797                    <div>
3798                      <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
3799                    </div>
3800                  );
3801                };
3802            "#},
3803            Mode::Normal,
3804        );
3805        cx.simulate_keystrokes("v a f");
3806        cx.assert_state(
3807            indoc! {r#"
3808                export const MyComponent = () => {
3809                  return (
3810                    <div>
3811                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3812                    </div>
3813                  );
3814                };
3815            "#},
3816            Mode::VisualLine,
3817        );
3818
3819        cx.set_state(
3820            indoc! {r#"
3821                export const MyComponent = () => {
3822                  return (
3823                    <div>
3824                      <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
3825                    </div>
3826                  );
3827                };
3828            "#},
3829            Mode::Normal,
3830        );
3831        cx.simulate_keystrokes("v a f");
3832        cx.assert_state(
3833            indoc! {r#"
3834                export const MyComponent = () => {
3835                  return (
3836                    <div>
3837                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3838                    </div>
3839                  );
3840                };
3841            "#},
3842            Mode::VisualLine,
3843        );
3844
3845        cx.set_state(
3846            indoc! {r#"
3847                export const MyComponent = () => {
3848                  return (
3849                    <div>
3850                      <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
3851                    </div>
3852                  );
3853                };
3854            "#},
3855            Mode::Normal,
3856        );
3857        cx.simulate_keystrokes("v a f");
3858        cx.assert_state(
3859            indoc! {r#"
3860                export const MyComponent = () => {
3861                  return (
3862                    <div>
3863                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3864                    </div>
3865                  );
3866                };
3867            "#},
3868            Mode::VisualLine,
3869        );
3870
3871        cx.set_state(
3872            indoc! {r#"
3873                export const MyComponent = () => {
3874                  return (
3875                    <div>
3876                      <div onClick={() => {
3877                        console.log("cliˇcked");
3878                      }}>Hello world!</div>
3879                    </div>
3880                  );
3881                };
3882            "#},
3883            Mode::Normal,
3884        );
3885        cx.simulate_keystrokes("v a f");
3886        cx.assert_state(
3887            indoc! {r#"
3888                export const MyComponent = () => {
3889                  return (
3890                    <div>
3891                      <div onClick={«() => {
3892                        console.log("clicked");
3893                      }ˇ»}>Hello world!</div>
3894                    </div>
3895                  );
3896                };
3897            "#},
3898            Mode::VisualLine,
3899        );
3900
3901        cx.set_state(
3902            indoc! {r#"
3903                export const MyComponent = () => {
3904                  return (
3905                    <div>
3906                      <div onClick={() => fˇoo()}>Hello world!</div>
3907                    </div>
3908                  );
3909                };
3910            "#},
3911            Mode::Normal,
3912        );
3913        cx.simulate_keystrokes("v a f");
3914        cx.assert_state(
3915            indoc! {r#"
3916                export const MyComponent = () => {
3917                  return (
3918                    <div>
3919                      <div onClick={«() => foo()ˇ»}>Hello world!</div>
3920                    </div>
3921                  );
3922                };
3923            "#},
3924            Mode::VisualLine,
3925        );
3926    }
3927
3928    #[gpui::test]
3929    async fn test_subword_object(cx: &mut gpui::TestAppContext) {
3930        let mut cx = VimTestContext::new(cx, true).await;
3931
3932        // Setup custom keybindings for subword object so we can use the
3933        // bindings in `simulate_keystrokes`.
3934        cx.update(|_window, cx| {
3935            cx.bind_keys([KeyBinding::new(
3936                "w",
3937                super::Subword {
3938                    ignore_punctuation: false,
3939                },
3940                Some("vim_operator"),
3941            )]);
3942        });
3943
3944        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3945        cx.simulate_keystrokes("c i w");
3946        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3947
3948        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
3949        cx.simulate_keystrokes("c i w");
3950        cx.assert_state("ˇ_bar_baz", Mode::Insert);
3951
3952        cx.set_state("foo_bar_baˇz", Mode::Normal);
3953        cx.simulate_keystrokes("c i w");
3954        cx.assert_state("foo_bar_ˇ", Mode::Insert);
3955
3956        cx.set_state("fooˇBarBaz", Mode::Normal);
3957        cx.simulate_keystrokes("c i w");
3958        cx.assert_state("fooˇBaz", Mode::Insert);
3959
3960        cx.set_state("ˇfooBarBaz", Mode::Normal);
3961        cx.simulate_keystrokes("c i w");
3962        cx.assert_state("ˇBarBaz", Mode::Insert);
3963
3964        cx.set_state("fooBarBaˇz", Mode::Normal);
3965        cx.simulate_keystrokes("c i w");
3966        cx.assert_state("fooBarˇ", Mode::Insert);
3967
3968        cx.set_state("foo.ˇbar.baz", Mode::Normal);
3969        cx.simulate_keystrokes("c i w");
3970        cx.assert_state("foo.ˇ.baz", Mode::Insert);
3971
3972        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3973        cx.simulate_keystrokes("d i w");
3974        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3975
3976        cx.set_state("fooˇBarBaz", Mode::Normal);
3977        cx.simulate_keystrokes("d i w");
3978        cx.assert_state("fooˇBaz", Mode::Normal);
3979
3980        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3981        cx.simulate_keystrokes("c a w");
3982        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3983
3984        cx.set_state("fooˇBarBaz", Mode::Normal);
3985        cx.simulate_keystrokes("c a w");
3986        cx.assert_state("fooˇBaz", Mode::Insert);
3987
3988        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3989        cx.simulate_keystrokes("d a w");
3990        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3991
3992        cx.set_state("fooˇBarBaz", Mode::Normal);
3993        cx.simulate_keystrokes("d a w");
3994        cx.assert_state("fooˇBaz", Mode::Normal);
3995    }
3996}