object.rs

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