object.rs

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