object.rs

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