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