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