object.rs

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