object.rs

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