object.rs

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