object.rs

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