object.rs

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