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,
  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)]
  22pub enum Object {
  23    Word { ignore_punctuation: bool },
  24    Subword { ignore_punctuation: bool },
  25    Sentence,
  26    Paragraph,
  27    Quotes,
  28    BackQuotes,
  29    AnyQuotes,
  30    DoubleQuotes,
  31    VerticalBars,
  32    Parentheses,
  33    SquareBrackets,
  34    CurlyBrackets,
  35    AngleBrackets,
  36    Argument,
  37    IndentObj { include_below: bool },
  38    Tag,
  39    Method,
  40    Class,
  41    Comment,
  42}
  43
  44#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  45#[serde(rename_all = "camelCase")]
  46struct Word {
  47    #[serde(default)]
  48    ignore_punctuation: bool,
  49}
  50
  51#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  52#[serde(rename_all = "camelCase")]
  53struct Subword {
  54    #[serde(default)]
  55    ignore_punctuation: bool,
  56}
  57#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  58#[serde(rename_all = "camelCase")]
  59struct IndentObj {
  60    #[serde(default)]
  61    include_below: bool,
  62}
  63
  64impl_actions!(vim, [Word, Subword, IndentObj]);
  65
  66actions!(
  67    vim,
  68    [
  69        Sentence,
  70        Paragraph,
  71        Quotes,
  72        BackQuotes,
  73        AnyQuotes,
  74        DoubleQuotes,
  75        VerticalBars,
  76        Parentheses,
  77        SquareBrackets,
  78        CurlyBrackets,
  79        AngleBrackets,
  80        Argument,
  81        Tag,
  82        Method,
  83        Class,
  84        Comment
  85    ]
  86);
  87
  88pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  89    Vim::action(
  90        editor,
  91        cx,
  92        |vim, &Word { ignore_punctuation }: &Word, window, cx| {
  93            vim.object(Object::Word { ignore_punctuation }, window, cx)
  94        },
  95    );
  96    Vim::action(
  97        editor,
  98        cx,
  99        |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
 100            vim.object(Object::Subword { ignore_punctuation }, window, cx)
 101        },
 102    );
 103    Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
 104        vim.object(Object::Tag, window, cx)
 105    });
 106    Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
 107        vim.object(Object::Sentence, window, cx)
 108    });
 109    Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
 110        vim.object(Object::Paragraph, window, cx)
 111    });
 112    Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
 113        vim.object(Object::Quotes, window, cx)
 114    });
 115    Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
 116        vim.object(Object::AnyQuotes, window, cx)
 117    });
 118    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 119        vim.object(Object::DoubleQuotes, window, cx)
 120    });
 121    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 122        vim.object(Object::DoubleQuotes, window, cx)
 123    });
 124    Vim::action(editor, cx, |vim, _: &Parentheses, window, cx| {
 125        vim.object(Object::Parentheses, window, cx)
 126    });
 127    Vim::action(editor, cx, |vim, _: &SquareBrackets, window, cx| {
 128        vim.object(Object::SquareBrackets, window, cx)
 129    });
 130    Vim::action(editor, cx, |vim, _: &CurlyBrackets, window, cx| {
 131        vim.object(Object::CurlyBrackets, window, cx)
 132    });
 133    Vim::action(editor, cx, |vim, _: &AngleBrackets, window, cx| {
 134        vim.object(Object::AngleBrackets, window, cx)
 135    });
 136    Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
 137        vim.object(Object::VerticalBars, window, cx)
 138    });
 139    Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
 140        vim.object(Object::Argument, window, cx)
 141    });
 142    Vim::action(editor, cx, |vim, _: &Method, window, cx| {
 143        vim.object(Object::Method, window, cx)
 144    });
 145    Vim::action(editor, cx, |vim, _: &Class, window, cx| {
 146        vim.object(Object::Class, window, cx)
 147    });
 148    Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
 149        if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
 150            vim.push_operator(Operator::Object { around: true }, window, cx);
 151        }
 152        vim.object(Object::Comment, window, cx)
 153    });
 154    Vim::action(
 155        editor,
 156        cx,
 157        |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
 158            vim.object(Object::IndentObj { include_below }, window, cx)
 159        },
 160    );
 161}
 162
 163impl Vim {
 164    fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 165        match self.mode {
 166            Mode::Normal => self.normal_object(object, window, cx),
 167            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 168                self.visual_object(object, window, cx)
 169            }
 170            Mode::Insert | Mode::Replace | Mode::HelixNormal => {
 171                // Shouldn't execute a text object in insert mode. Ignoring
 172            }
 173        }
 174    }
 175}
 176
 177impl Object {
 178    pub fn is_multiline(self) -> bool {
 179        match self {
 180            Object::Word { .. }
 181            | Object::Subword { .. }
 182            | Object::Quotes
 183            | Object::BackQuotes
 184            | Object::AnyQuotes
 185            | Object::VerticalBars
 186            | Object::DoubleQuotes => false,
 187            Object::Sentence
 188            | Object::Paragraph
 189            | Object::Parentheses
 190            | Object::Tag
 191            | Object::AngleBrackets
 192            | Object::CurlyBrackets
 193            | Object::SquareBrackets
 194            | Object::Argument
 195            | Object::Method
 196            | Object::Class
 197            | Object::Comment
 198            | Object::IndentObj { .. } => true,
 199        }
 200    }
 201
 202    pub fn always_expands_both_ways(self) -> bool {
 203        match self {
 204            Object::Word { .. }
 205            | Object::Subword { .. }
 206            | Object::Sentence
 207            | Object::Paragraph
 208            | Object::Argument
 209            | Object::IndentObj { .. } => false,
 210            Object::Quotes
 211            | Object::BackQuotes
 212            | Object::AnyQuotes
 213            | Object::DoubleQuotes
 214            | Object::VerticalBars
 215            | Object::Parentheses
 216            | Object::SquareBrackets
 217            | Object::Tag
 218            | Object::Method
 219            | Object::Class
 220            | Object::Comment
 221            | Object::CurlyBrackets
 222            | Object::AngleBrackets => true,
 223        }
 224    }
 225
 226    pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
 227        match self {
 228            Object::Word { .. }
 229            | Object::Subword { .. }
 230            | Object::Sentence
 231            | Object::Quotes
 232            | Object::AnyQuotes
 233            | Object::BackQuotes
 234            | Object::DoubleQuotes => {
 235                if current_mode == Mode::VisualBlock {
 236                    Mode::VisualBlock
 237                } else {
 238                    Mode::Visual
 239                }
 240            }
 241            Object::Parentheses
 242            | Object::SquareBrackets
 243            | Object::CurlyBrackets
 244            | Object::AngleBrackets
 245            | Object::VerticalBars
 246            | Object::Tag
 247            | Object::Comment
 248            | Object::Argument
 249            | Object::IndentObj { .. } => Mode::Visual,
 250            Object::Method | Object::Class => {
 251                if around {
 252                    Mode::VisualLine
 253                } else {
 254                    Mode::Visual
 255                }
 256            }
 257            Object::Paragraph => Mode::VisualLine,
 258        }
 259    }
 260
 261    pub fn range(
 262        self,
 263        map: &DisplaySnapshot,
 264        selection: Selection<DisplayPoint>,
 265        around: bool,
 266    ) -> Option<Range<DisplayPoint>> {
 267        let relative_to = selection.head();
 268        match self {
 269            Object::Word { ignore_punctuation } => {
 270                if around {
 271                    around_word(map, relative_to, ignore_punctuation)
 272                } else {
 273                    in_word(map, relative_to, ignore_punctuation)
 274                }
 275            }
 276            Object::Subword { ignore_punctuation } => {
 277                if around {
 278                    around_subword(map, relative_to, ignore_punctuation)
 279                } else {
 280                    in_subword(map, relative_to, ignore_punctuation)
 281                }
 282            }
 283            Object::Sentence => sentence(map, relative_to, around),
 284            Object::Paragraph => paragraph(map, relative_to, around),
 285            Object::Quotes => {
 286                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 287            }
 288            Object::BackQuotes => {
 289                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 290            }
 291            Object::AnyQuotes => {
 292                let quote_types = ['\'', '"', '`']; // Types of quotes to handle
 293                let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
 294
 295                // Find the closest matching quote range
 296                quote_types
 297                    .iter()
 298                    .flat_map(|&quote| {
 299                        // Get ranges for each quote type
 300                        surrounding_markers(
 301                            map,
 302                            relative_to,
 303                            around,
 304                            self.is_multiline(),
 305                            quote,
 306                            quote,
 307                        )
 308                    })
 309                    .min_by_key(|range| {
 310                        // Calculate proximity of ranges to the cursor
 311                        let start_distance = (relative_offset
 312                            - range.start.to_offset(map, Bias::Left) as isize)
 313                            .abs();
 314                        let end_distance = (relative_offset
 315                            - range.end.to_offset(map, Bias::Right) as isize)
 316                            .abs();
 317                        start_distance + end_distance
 318                    })
 319            }
 320            Object::DoubleQuotes => {
 321                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 322            }
 323            Object::VerticalBars => {
 324                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 325            }
 326            Object::Parentheses => {
 327                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 328            }
 329            Object::Tag => {
 330                let head = selection.head();
 331                let range = selection.range();
 332                surrounding_html_tag(map, head, range, around)
 333            }
 334            Object::SquareBrackets => {
 335                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 336            }
 337            Object::CurlyBrackets => {
 338                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 339            }
 340            Object::AngleBrackets => {
 341                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 342            }
 343            Object::Method => text_object(
 344                map,
 345                relative_to,
 346                if around {
 347                    TextObject::AroundFunction
 348                } else {
 349                    TextObject::InsideFunction
 350                },
 351            ),
 352            Object::Comment => text_object(
 353                map,
 354                relative_to,
 355                if around {
 356                    TextObject::AroundComment
 357                } else {
 358                    TextObject::InsideComment
 359                },
 360            ),
 361            Object::Class => text_object(
 362                map,
 363                relative_to,
 364                if around {
 365                    TextObject::AroundClass
 366                } else {
 367                    TextObject::InsideClass
 368                },
 369            ),
 370            Object::Argument => argument(map, relative_to, around),
 371            Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
 372        }
 373    }
 374
 375    pub fn expand_selection(
 376        self,
 377        map: &DisplaySnapshot,
 378        selection: &mut Selection<DisplayPoint>,
 379        around: bool,
 380    ) -> bool {
 381        if let Some(range) = self.range(map, selection.clone(), around) {
 382            selection.start = range.start;
 383            selection.end = range.end;
 384            true
 385        } else {
 386            false
 387        }
 388    }
 389}
 390
 391/// Returns a range that surrounds the word `relative_to` is in.
 392///
 393/// If `relative_to` is at the start of a word, return the word.
 394/// If `relative_to` is between words, return the space between.
 395fn in_word(
 396    map: &DisplaySnapshot,
 397    relative_to: DisplayPoint,
 398    ignore_punctuation: bool,
 399) -> Option<Range<DisplayPoint>> {
 400    // Use motion::right so that we consider the character under the cursor when looking for the start
 401    let classifier = map
 402        .buffer_snapshot
 403        .char_classifier_at(relative_to.to_point(map))
 404        .ignore_punctuation(ignore_punctuation);
 405    let start = movement::find_preceding_boundary_display_point(
 406        map,
 407        right(map, relative_to, 1),
 408        movement::FindRange::SingleLine,
 409        |left, right| classifier.kind(left) != classifier.kind(right),
 410    );
 411
 412    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 413        classifier.kind(left) != classifier.kind(right)
 414    });
 415
 416    Some(start..end)
 417}
 418
 419fn in_subword(
 420    map: &DisplaySnapshot,
 421    relative_to: DisplayPoint,
 422    ignore_punctuation: bool,
 423) -> Option<Range<DisplayPoint>> {
 424    let offset = relative_to.to_offset(map, Bias::Left);
 425    // Use motion::right so that we consider the character under the cursor when looking for the start
 426    let classifier = map
 427        .buffer_snapshot
 428        .char_classifier_at(relative_to.to_point(map))
 429        .ignore_punctuation(ignore_punctuation);
 430    let in_subword = map
 431        .buffer_chars_at(offset)
 432        .next()
 433        .map(|(c, _)| {
 434            if classifier.is_word('-') {
 435                !classifier.is_whitespace(c) && c != '_' && c != '-'
 436            } else {
 437                !classifier.is_whitespace(c) && c != '_'
 438            }
 439        })
 440        .unwrap_or(false);
 441
 442    let start = if in_subword {
 443        movement::find_preceding_boundary_display_point(
 444            map,
 445            right(map, relative_to, 1),
 446            movement::FindRange::SingleLine,
 447            |left, right| {
 448                let is_word_start = classifier.kind(left) != classifier.kind(right);
 449                let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
 450                    || left == '_' && right != '_'
 451                    || left.is_lowercase() && right.is_uppercase();
 452                is_word_start || is_subword_start
 453            },
 454        )
 455    } else {
 456        movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 457            let is_word_start = classifier.kind(left) != classifier.kind(right);
 458            let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
 459                || left == '_' && right != '_'
 460                || left.is_lowercase() && right.is_uppercase();
 461            is_word_start || is_subword_start
 462        })
 463    };
 464
 465    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 466        let is_word_end = classifier.kind(left) != classifier.kind(right);
 467        let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
 468            || left != '_' && right == '_'
 469            || left.is_lowercase() && right.is_uppercase();
 470        is_word_end || is_subword_end
 471    });
 472
 473    Some(start..end)
 474}
 475
 476pub fn surrounding_html_tag(
 477    map: &DisplaySnapshot,
 478    head: DisplayPoint,
 479    range: Range<DisplayPoint>,
 480    around: bool,
 481) -> Option<Range<DisplayPoint>> {
 482    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 483        chars
 484            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 485            .collect()
 486    }
 487    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 488        if Some('<') != chars.next() {
 489            return None;
 490        }
 491        Some(read_tag(chars))
 492    }
 493    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 494        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 495            return None;
 496        }
 497        Some(read_tag(chars))
 498    }
 499
 500    let snapshot = &map.buffer_snapshot;
 501    let offset = head.to_offset(map, Bias::Left);
 502    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 503    let buffer = excerpt.buffer();
 504    let offset = excerpt.map_offset_to_buffer(offset);
 505
 506    // Find the most closest to current offset
 507    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 508    let mut last_child_node = cursor.node();
 509    while cursor.goto_first_child_for_byte(offset).is_some() {
 510        last_child_node = cursor.node();
 511    }
 512
 513    let mut last_child_node = Some(last_child_node);
 514    while let Some(cur_node) = last_child_node {
 515        if cur_node.child_count() >= 2 {
 516            let first_child = cur_node.child(0);
 517            let last_child = cur_node.child(cur_node.child_count() - 1);
 518            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 519                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 520                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 521                // It needs to be handled differently according to the selection length
 522                let is_valid = if range.end.to_offset(map, Bias::Left)
 523                    - range.start.to_offset(map, Bias::Left)
 524                    <= 1
 525                {
 526                    offset <= last_child.end_byte()
 527                } else {
 528                    range.start.to_offset(map, Bias::Left) >= first_child.start_byte()
 529                        && range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
 530                };
 531                if open_tag.is_some() && open_tag == close_tag && is_valid {
 532                    let range = if around {
 533                        first_child.byte_range().start..last_child.byte_range().end
 534                    } else {
 535                        first_child.byte_range().end..last_child.byte_range().start
 536                    };
 537                    if excerpt.contains_buffer_range(range.clone()) {
 538                        let result = excerpt.map_range_from_buffer(range);
 539                        return Some(
 540                            result.start.to_display_point(map)..result.end.to_display_point(map),
 541                        );
 542                    }
 543                }
 544            }
 545        }
 546        last_child_node = cur_node.parent();
 547    }
 548    None
 549}
 550
 551/// Returns a range that surrounds the word and following whitespace
 552/// relative_to is in.
 553///
 554/// If `relative_to` is at the start of a word, return the word and following whitespace.
 555/// If `relative_to` is between words, return the whitespace back and the following word.
 556///
 557/// if in word
 558///   delete that word
 559///   if there is whitespace following the word, delete that as well
 560///   otherwise, delete any preceding whitespace
 561/// otherwise
 562///   delete whitespace around cursor
 563///   delete word following the cursor
 564fn around_word(
 565    map: &DisplaySnapshot,
 566    relative_to: DisplayPoint,
 567    ignore_punctuation: bool,
 568) -> Option<Range<DisplayPoint>> {
 569    let offset = relative_to.to_offset(map, Bias::Left);
 570    let classifier = map
 571        .buffer_snapshot
 572        .char_classifier_at(offset)
 573        .ignore_punctuation(ignore_punctuation);
 574    let in_word = map
 575        .buffer_chars_at(offset)
 576        .next()
 577        .map(|(c, _)| !classifier.is_whitespace(c))
 578        .unwrap_or(false);
 579
 580    if in_word {
 581        around_containing_word(map, relative_to, ignore_punctuation)
 582    } else {
 583        around_next_word(map, relative_to, ignore_punctuation)
 584    }
 585}
 586
 587fn around_subword(
 588    map: &DisplaySnapshot,
 589    relative_to: DisplayPoint,
 590    ignore_punctuation: bool,
 591) -> Option<Range<DisplayPoint>> {
 592    // Use motion::right so that we consider the character under the cursor when looking for the start
 593    let classifier = map
 594        .buffer_snapshot
 595        .char_classifier_at(relative_to.to_point(map))
 596        .ignore_punctuation(ignore_punctuation);
 597    let start = movement::find_preceding_boundary_display_point(
 598        map,
 599        right(map, relative_to, 1),
 600        movement::FindRange::SingleLine,
 601        |left, right| {
 602            let is_word_start = classifier.kind(left) != classifier.kind(right);
 603            let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
 604                || left != '_' && right == '_'
 605                || left.is_lowercase() && right.is_uppercase();
 606            is_word_start || is_subword_start
 607        },
 608    );
 609
 610    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 611        let is_word_end = classifier.kind(left) != classifier.kind(right);
 612        let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
 613            || left != '_' && right == '_'
 614            || left.is_lowercase() && right.is_uppercase();
 615        is_word_end || is_subword_end
 616    });
 617
 618    Some(start..end)
 619}
 620
 621fn around_containing_word(
 622    map: &DisplaySnapshot,
 623    relative_to: DisplayPoint,
 624    ignore_punctuation: bool,
 625) -> Option<Range<DisplayPoint>> {
 626    in_word(map, relative_to, ignore_punctuation)
 627        .map(|range| expand_to_include_whitespace(map, range, true))
 628}
 629
 630fn around_next_word(
 631    map: &DisplaySnapshot,
 632    relative_to: DisplayPoint,
 633    ignore_punctuation: bool,
 634) -> Option<Range<DisplayPoint>> {
 635    let classifier = map
 636        .buffer_snapshot
 637        .char_classifier_at(relative_to.to_point(map))
 638        .ignore_punctuation(ignore_punctuation);
 639    // Get the start of the word
 640    let start = movement::find_preceding_boundary_display_point(
 641        map,
 642        right(map, relative_to, 1),
 643        FindRange::SingleLine,
 644        |left, right| classifier.kind(left) != classifier.kind(right),
 645    );
 646
 647    let mut word_found = false;
 648    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 649        let left_kind = classifier.kind(left);
 650        let right_kind = classifier.kind(right);
 651
 652        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 653
 654        if right_kind != CharKind::Whitespace {
 655            word_found = true;
 656        }
 657
 658        found
 659    });
 660
 661    Some(start..end)
 662}
 663
 664fn text_object(
 665    map: &DisplaySnapshot,
 666    relative_to: DisplayPoint,
 667    target: TextObject,
 668) -> Option<Range<DisplayPoint>> {
 669    let snapshot = &map.buffer_snapshot;
 670    let offset = relative_to.to_offset(map, Bias::Left);
 671
 672    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 673    let buffer = excerpt.buffer();
 674    let offset = excerpt.map_offset_to_buffer(offset);
 675
 676    let mut matches: Vec<Range<usize>> = buffer
 677        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 678        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 679        .collect();
 680    matches.sort_by_key(|r| (r.end - r.start));
 681    if let Some(buffer_range) = matches.first() {
 682        let range = excerpt.map_range_from_buffer(buffer_range.clone());
 683        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 684    }
 685
 686    let around = target.around()?;
 687    let mut matches: Vec<Range<usize>> = buffer
 688        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 689        .filter_map(|(r, m)| if m == around { Some(r) } else { None })
 690        .collect();
 691    matches.sort_by_key(|r| (r.end - r.start));
 692    let around_range = matches.first()?;
 693
 694    let mut matches: Vec<Range<usize>> = buffer
 695        .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
 696        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 697        .collect();
 698    matches.sort_by_key(|r| r.start);
 699    if let Some(buffer_range) = matches.first() {
 700        if !buffer_range.is_empty() {
 701            let range = excerpt.map_range_from_buffer(buffer_range.clone());
 702            return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 703        }
 704    }
 705    let buffer_range = excerpt.map_range_from_buffer(around_range.clone());
 706    return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
 707}
 708
 709fn argument(
 710    map: &DisplaySnapshot,
 711    relative_to: DisplayPoint,
 712    around: bool,
 713) -> Option<Range<DisplayPoint>> {
 714    let snapshot = &map.buffer_snapshot;
 715    let offset = relative_to.to_offset(map, Bias::Left);
 716
 717    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 718    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 719    let buffer = excerpt.buffer();
 720
 721    fn comma_delimited_range_at(
 722        buffer: &BufferSnapshot,
 723        mut offset: usize,
 724        include_comma: bool,
 725    ) -> Option<Range<usize>> {
 726        // Seek to the first non-whitespace character
 727        offset += buffer
 728            .chars_at(offset)
 729            .take_while(|c| c.is_whitespace())
 730            .map(char::len_utf8)
 731            .sum::<usize>();
 732
 733        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 734            // Filter out empty ranges
 735            if open.end == close.start {
 736                return false;
 737            }
 738
 739            // If the cursor is outside the brackets, ignore them
 740            if open.start == offset || close.end == offset {
 741                return false;
 742            }
 743
 744            // TODO: Is there any better way to filter out string brackets?
 745            // Used to filter out string brackets
 746            matches!(
 747                buffer.chars_at(open.start).next(),
 748                Some('(' | '[' | '{' | '<' | '|')
 749            )
 750        };
 751
 752        // Find the brackets containing the cursor
 753        let (open_bracket, close_bracket) =
 754            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 755
 756        let inner_bracket_range = open_bracket.end..close_bracket.start;
 757
 758        let layer = buffer.syntax_layer_at(offset)?;
 759        let node = layer.node();
 760        let mut cursor = node.walk();
 761
 762        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 763        let mut parent_covers_bracket_range = false;
 764        loop {
 765            let node = cursor.node();
 766            let range = node.byte_range();
 767            let covers_bracket_range =
 768                range.start == open_bracket.start && range.end == close_bracket.end;
 769            if parent_covers_bracket_range && !covers_bracket_range {
 770                break;
 771            }
 772            parent_covers_bracket_range = covers_bracket_range;
 773
 774            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 775            cursor.goto_first_child_for_byte(offset)?;
 776        }
 777
 778        let mut argument_node = cursor.node();
 779
 780        // If the child node is the open bracket, move to the next sibling.
 781        if argument_node.byte_range() == open_bracket {
 782            if !cursor.goto_next_sibling() {
 783                return Some(inner_bracket_range);
 784            }
 785            argument_node = cursor.node();
 786        }
 787        // While the child node is the close bracket or a comma, move to the previous sibling
 788        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 789            if !cursor.goto_previous_sibling() {
 790                return Some(inner_bracket_range);
 791            }
 792            argument_node = cursor.node();
 793            if argument_node.byte_range() == open_bracket {
 794                return Some(inner_bracket_range);
 795            }
 796        }
 797
 798        // The start and end of the argument range, defaulting to the start and end of the argument node
 799        let mut start = argument_node.start_byte();
 800        let mut end = argument_node.end_byte();
 801
 802        let mut needs_surrounding_comma = include_comma;
 803
 804        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 805        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 806        while cursor.goto_previous_sibling() {
 807            let prev = cursor.node();
 808
 809            if prev.start_byte() < open_bracket.end {
 810                start = open_bracket.end;
 811                break;
 812            } else if prev.kind() == "," {
 813                if needs_surrounding_comma {
 814                    start = prev.start_byte();
 815                    needs_surrounding_comma = false;
 816                }
 817                break;
 818            } else if prev.start_byte() < start {
 819                start = prev.start_byte();
 820            }
 821        }
 822
 823        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 824        while cursor.goto_next_sibling() {
 825            let next = cursor.node();
 826
 827            if next.end_byte() > close_bracket.start {
 828                end = close_bracket.start;
 829                break;
 830            } else if next.kind() == "," {
 831                if needs_surrounding_comma {
 832                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 833                    if let Some(next_arg) = next.next_sibling() {
 834                        end = next_arg.start_byte();
 835                    } else {
 836                        end = next.end_byte();
 837                    }
 838                }
 839                break;
 840            } else if next.end_byte() > end {
 841                end = next.end_byte();
 842            }
 843        }
 844
 845        Some(start..end)
 846    }
 847
 848    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 849
 850    if excerpt.contains_buffer_range(result.clone()) {
 851        let result = excerpt.map_range_from_buffer(result);
 852        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 853    } else {
 854        None
 855    }
 856}
 857
 858fn indent(
 859    map: &DisplaySnapshot,
 860    relative_to: DisplayPoint,
 861    around: bool,
 862    include_below: bool,
 863) -> Option<Range<DisplayPoint>> {
 864    let point = relative_to.to_point(map);
 865    let row = point.row;
 866
 867    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
 868
 869    // Loop backwards until we find a non-blank line with less indent
 870    let mut start_row = row;
 871    for prev_row in (0..row).rev() {
 872        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
 873        if indent.is_line_empty() {
 874            continue;
 875        }
 876        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 877            if around {
 878                // When around is true, include the first line with less indent
 879                start_row = prev_row;
 880            }
 881            break;
 882        }
 883        start_row = prev_row;
 884    }
 885
 886    // Loop forwards until we find a non-blank line with less indent
 887    let mut end_row = row;
 888    let max_rows = map.buffer_snapshot.max_row().0;
 889    for next_row in (row + 1)..=max_rows {
 890        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
 891        if indent.is_line_empty() {
 892            continue;
 893        }
 894        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 895            if around && include_below {
 896                // When around is true and including below, include this line
 897                end_row = next_row;
 898            }
 899            break;
 900        }
 901        end_row = next_row;
 902    }
 903
 904    let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
 905    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
 906    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
 907    Some(start..end)
 908}
 909
 910fn sentence(
 911    map: &DisplaySnapshot,
 912    relative_to: DisplayPoint,
 913    around: bool,
 914) -> Option<Range<DisplayPoint>> {
 915    let mut start = None;
 916    let relative_offset = relative_to.to_offset(map, Bias::Left);
 917    let mut previous_end = relative_offset;
 918
 919    let mut chars = map.buffer_chars_at(previous_end).peekable();
 920
 921    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 922    for (char, offset) in chars
 923        .peek()
 924        .cloned()
 925        .into_iter()
 926        .chain(map.reverse_buffer_chars_at(previous_end))
 927    {
 928        if is_sentence_end(map, offset) {
 929            break;
 930        }
 931
 932        if is_possible_sentence_start(char) {
 933            start = Some(offset);
 934        }
 935
 936        previous_end = offset;
 937    }
 938
 939    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 940    let mut end = relative_offset;
 941    for (char, offset) in chars {
 942        if start.is_none() && is_possible_sentence_start(char) {
 943            if around {
 944                start = Some(offset);
 945                continue;
 946            } else {
 947                end = offset;
 948                break;
 949            }
 950        }
 951
 952        if char != '\n' {
 953            end = offset + char.len_utf8();
 954        }
 955
 956        if is_sentence_end(map, end) {
 957            break;
 958        }
 959    }
 960
 961    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
 962    if around {
 963        range = expand_to_include_whitespace(map, range, false);
 964    }
 965
 966    Some(range)
 967}
 968
 969fn is_possible_sentence_start(character: char) -> bool {
 970    !character.is_whitespace() && character != '.'
 971}
 972
 973const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 974const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 975const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 976fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
 977    let mut next_chars = map.buffer_chars_at(offset).peekable();
 978    if let Some((char, _)) = next_chars.next() {
 979        // We are at a double newline. This position is a sentence end.
 980        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 981            return true;
 982        }
 983
 984        // The next text is not a valid whitespace. This is not a sentence end
 985        if !SENTENCE_END_WHITESPACE.contains(&char) {
 986            return false;
 987        }
 988    }
 989
 990    for (char, _) in map.reverse_buffer_chars_at(offset) {
 991        if SENTENCE_END_PUNCTUATION.contains(&char) {
 992            return true;
 993        }
 994
 995        if !SENTENCE_END_FILLERS.contains(&char) {
 996            return false;
 997        }
 998    }
 999
1000    false
1001}
1002
1003/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1004/// whitespace to the end first and falls back to the start if there was none.
1005fn expand_to_include_whitespace(
1006    map: &DisplaySnapshot,
1007    range: Range<DisplayPoint>,
1008    stop_at_newline: bool,
1009) -> Range<DisplayPoint> {
1010    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1011    let mut whitespace_included = false;
1012
1013    let chars = map.buffer_chars_at(range.end).peekable();
1014    for (char, offset) in chars {
1015        if char == '\n' && stop_at_newline {
1016            break;
1017        }
1018
1019        if char.is_whitespace() {
1020            if char != '\n' {
1021                range.end = offset + char.len_utf8();
1022                whitespace_included = true;
1023            }
1024        } else {
1025            // Found non whitespace. Quit out.
1026            break;
1027        }
1028    }
1029
1030    if !whitespace_included {
1031        for (char, point) in map.reverse_buffer_chars_at(range.start) {
1032            if char == '\n' && stop_at_newline {
1033                break;
1034            }
1035
1036            if !char.is_whitespace() {
1037                break;
1038            }
1039
1040            range.start = point;
1041        }
1042    }
1043
1044    range.start.to_display_point(map)..range.end.to_display_point(map)
1045}
1046
1047/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1048/// where `relative_to` is in. If `around`, principally returns the range ending
1049/// at the end of the next paragraph.
1050///
1051/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1052/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1053/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1054/// the trailing newline is not subject to subsequent operations).
1055///
1056/// Edge cases:
1057/// - If `around` and if the current paragraph is the last paragraph of the
1058///   file and is blank, then the selection results in an error.
1059/// - If `around` and if the current paragraph is the last paragraph of the
1060///   file and is not blank, then the returned range starts at the start of the
1061///   previous paragraph, if it exists.
1062fn paragraph(
1063    map: &DisplaySnapshot,
1064    relative_to: DisplayPoint,
1065    around: bool,
1066) -> Option<Range<DisplayPoint>> {
1067    let mut paragraph_start = start_of_paragraph(map, relative_to);
1068    let mut paragraph_end = end_of_paragraph(map, relative_to);
1069
1070    let paragraph_end_row = paragraph_end.row();
1071    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1072    let point = relative_to.to_point(map);
1073    let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1074
1075    if around {
1076        if paragraph_ends_with_eof {
1077            if current_line_is_empty {
1078                return None;
1079            }
1080
1081            let paragraph_start_row = paragraph_start.row();
1082            if paragraph_start_row.0 != 0 {
1083                let previous_paragraph_last_line_start =
1084                    DisplayPoint::new(paragraph_start_row - 1, 0);
1085                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1086            }
1087        } else {
1088            let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
1089            paragraph_end = end_of_paragraph(map, next_paragraph_start);
1090        }
1091    }
1092
1093    let range = paragraph_start..paragraph_end;
1094    Some(range)
1095}
1096
1097/// Returns a position of the start of the current paragraph, where a paragraph
1098/// is defined as a run of non-blank lines or a run of blank lines.
1099pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1100    let point = display_point.to_point(map);
1101    if point.row == 0 {
1102        return DisplayPoint::zero();
1103    }
1104
1105    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1106
1107    for row in (0..point.row).rev() {
1108        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1109        if blank != is_current_line_blank {
1110            return Point::new(row + 1, 0).to_display_point(map);
1111        }
1112    }
1113
1114    DisplayPoint::zero()
1115}
1116
1117/// Returns a position of the end of the current paragraph, where a paragraph
1118/// is defined as a run of non-blank lines or a run of blank lines.
1119/// The trailing newline is excluded from the paragraph.
1120pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1121    let point = display_point.to_point(map);
1122    if point.row == map.buffer_snapshot.max_row().0 {
1123        return map.max_point();
1124    }
1125
1126    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1127
1128    for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
1129        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1130        if blank != is_current_line_blank {
1131            let previous_row = row - 1;
1132            return Point::new(
1133                previous_row,
1134                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
1135            )
1136            .to_display_point(map);
1137        }
1138    }
1139
1140    map.max_point()
1141}
1142
1143fn surrounding_markers(
1144    map: &DisplaySnapshot,
1145    relative_to: DisplayPoint,
1146    around: bool,
1147    search_across_lines: bool,
1148    open_marker: char,
1149    close_marker: char,
1150) -> Option<Range<DisplayPoint>> {
1151    let point = relative_to.to_offset(map, Bias::Left);
1152
1153    let mut matched_closes = 0;
1154    let mut opening = None;
1155
1156    let mut before_ch = match movement::chars_before(map, point).next() {
1157        Some((ch, _)) => ch,
1158        _ => '\0',
1159    };
1160    if let Some((ch, range)) = movement::chars_after(map, point).next() {
1161        if ch == open_marker && before_ch != '\\' {
1162            if open_marker == close_marker {
1163                let mut total = 0;
1164                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1165                {
1166                    if ch == '\n' {
1167                        break;
1168                    }
1169                    if ch == open_marker && before_ch != '\\' {
1170                        total += 1;
1171                    }
1172                }
1173                if total % 2 == 0 {
1174                    opening = Some(range)
1175                }
1176            } else {
1177                opening = Some(range)
1178            }
1179        }
1180    }
1181
1182    if opening.is_none() {
1183        let mut chars_before = movement::chars_before(map, point).peekable();
1184        while let Some((ch, range)) = chars_before.next() {
1185            if ch == '\n' && !search_across_lines {
1186                break;
1187            }
1188
1189            if let Some((before_ch, _)) = chars_before.peek() {
1190                if *before_ch == '\\' {
1191                    continue;
1192                }
1193            }
1194
1195            if ch == open_marker {
1196                if matched_closes == 0 {
1197                    opening = Some(range);
1198                    break;
1199                }
1200                matched_closes -= 1;
1201            } else if ch == close_marker {
1202                matched_closes += 1
1203            }
1204        }
1205    }
1206    if opening.is_none() {
1207        for (ch, range) in movement::chars_after(map, point) {
1208            if before_ch != '\\' {
1209                if ch == open_marker {
1210                    opening = Some(range);
1211                    break;
1212                } else if ch == close_marker {
1213                    break;
1214                }
1215            }
1216
1217            before_ch = ch;
1218        }
1219    }
1220
1221    let mut opening = opening?;
1222
1223    let mut matched_opens = 0;
1224    let mut closing = None;
1225    before_ch = match movement::chars_before(map, opening.end).next() {
1226        Some((ch, _)) => ch,
1227        _ => '\0',
1228    };
1229    for (ch, range) in movement::chars_after(map, opening.end) {
1230        if ch == '\n' && !search_across_lines {
1231            break;
1232        }
1233
1234        if before_ch != '\\' {
1235            if ch == close_marker {
1236                if matched_opens == 0 {
1237                    closing = Some(range);
1238                    break;
1239                }
1240                matched_opens -= 1;
1241            } else if ch == open_marker {
1242                matched_opens += 1;
1243            }
1244        }
1245
1246        before_ch = ch;
1247    }
1248
1249    let mut closing = closing?;
1250
1251    if around && !search_across_lines {
1252        let mut found = false;
1253
1254        for (ch, range) in movement::chars_after(map, closing.end) {
1255            if ch.is_whitespace() && ch != '\n' {
1256                found = true;
1257                closing.end = range.end;
1258            } else {
1259                break;
1260            }
1261        }
1262
1263        if !found {
1264            for (ch, range) in movement::chars_before(map, opening.start) {
1265                if ch.is_whitespace() && ch != '\n' {
1266                    opening.start = range.start
1267                } else {
1268                    break;
1269                }
1270            }
1271        }
1272    }
1273
1274    if !around && search_across_lines {
1275        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1276            if ch == '\n' {
1277                opening.end = range.end
1278            }
1279        }
1280
1281        for (ch, range) in movement::chars_before(map, closing.start) {
1282            if !ch.is_whitespace() {
1283                break;
1284            }
1285            if ch != '\n' {
1286                closing.start = range.start
1287            }
1288        }
1289    }
1290
1291    let result = if around {
1292        opening.start..closing.end
1293    } else {
1294        opening.end..closing.start
1295    };
1296
1297    Some(
1298        map.clip_point(result.start.to_display_point(map), Bias::Left)
1299            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1300    )
1301}
1302
1303#[cfg(test)]
1304mod test {
1305    use indoc::indoc;
1306
1307    use crate::{
1308        state::Mode,
1309        test::{NeovimBackedTestContext, VimTestContext},
1310    };
1311
1312    const WORD_LOCATIONS: &str = indoc! {"
1313        The quick ˇbrowˇnˇ•••
1314        fox ˇjuˇmpsˇ over
1315        the lazy dogˇ••
1316        ˇ
1317        ˇ
1318        ˇ
1319        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1320        ˇ••
1321        ˇ••
1322        ˇ  fox-jumpˇs over
1323        the lazy dogˇ•
1324        ˇ
1325        "
1326    };
1327
1328    #[gpui::test]
1329    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1330        let mut cx = NeovimBackedTestContext::new(cx).await;
1331
1332        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1333            .await
1334            .assert_matches();
1335        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1336            .await
1337            .assert_matches();
1338        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1339            .await
1340            .assert_matches();
1341        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1342            .await
1343            .assert_matches();
1344    }
1345
1346    #[gpui::test]
1347    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1348        let mut cx = NeovimBackedTestContext::new(cx).await;
1349
1350        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1351            .await
1352            .assert_matches();
1353        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1354            .await
1355            .assert_matches();
1356        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1357            .await
1358            .assert_matches();
1359        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1360            .await
1361            .assert_matches();
1362    }
1363
1364    #[gpui::test]
1365    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1366        let mut cx = NeovimBackedTestContext::new(cx).await;
1367
1368        /*
1369                cx.set_shared_state("The quick ˇbrown\nfox").await;
1370                cx.simulate_shared_keystrokes(["v"]).await;
1371                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1372                cx.simulate_shared_keystrokes(["i", "w"]).await;
1373                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1374        */
1375        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1376        cx.simulate_shared_keystrokes("v").await;
1377        cx.shared_state()
1378            .await
1379            .assert_eq("The quick brown\n«\nˇ»fox");
1380        cx.simulate_shared_keystrokes("i w").await;
1381        cx.shared_state()
1382            .await
1383            .assert_eq("The quick brown\n«\nˇ»fox");
1384
1385        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1386            .await
1387            .assert_matches();
1388        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1389            .await
1390            .assert_matches();
1391    }
1392
1393    const PARAGRAPH_EXAMPLES: &[&str] = &[
1394        // Single line
1395        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1396        // Multiple lines without empty lines
1397        indoc! {"
1398            ˇThe quick brownˇ
1399            ˇfox jumps overˇ
1400            the lazy dog.ˇ
1401        "},
1402        // Heading blank paragraph and trailing normal paragraph
1403        indoc! {"
1404            ˇ
1405            ˇ
1406            ˇThe quick brown fox jumps
1407            ˇover the lazy dog.
1408            ˇ
1409            ˇ
1410            ˇThe quick brown fox jumpsˇ
1411            ˇover the lazy dog.ˇ
1412        "},
1413        // Inserted blank paragraph and trailing blank paragraph
1414        indoc! {"
1415            ˇThe quick brown fox jumps
1416            ˇover the lazy dog.
1417            ˇ
1418            ˇ
1419            ˇ
1420            ˇThe quick brown fox jumpsˇ
1421            ˇover the lazy dog.ˇ
1422            ˇ
1423            ˇ
1424            ˇ
1425        "},
1426        // "Blank" paragraph with whitespace characters
1427        indoc! {"
1428            ˇThe quick brown fox jumps
1429            over the lazy dog.
1430
1431            ˇ \t
1432
1433            ˇThe quick brown fox jumps
1434            over the lazy dog.ˇ
1435            ˇ
1436            ˇ \t
1437            \t \t
1438        "},
1439        // Single line "paragraphs", where selection size might be zero.
1440        indoc! {"
1441            ˇThe quick brown fox jumps over the lazy dog.
1442            ˇ
1443            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1444            ˇ
1445        "},
1446    ];
1447
1448    #[gpui::test]
1449    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1450        let mut cx = NeovimBackedTestContext::new(cx).await;
1451
1452        for paragraph_example in PARAGRAPH_EXAMPLES {
1453            cx.simulate_at_each_offset("c i p", paragraph_example)
1454                .await
1455                .assert_matches();
1456            cx.simulate_at_each_offset("c a p", paragraph_example)
1457                .await
1458                .assert_matches();
1459        }
1460    }
1461
1462    #[gpui::test]
1463    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1464        let mut cx = NeovimBackedTestContext::new(cx).await;
1465
1466        for paragraph_example in PARAGRAPH_EXAMPLES {
1467            cx.simulate_at_each_offset("d i p", paragraph_example)
1468                .await
1469                .assert_matches();
1470            cx.simulate_at_each_offset("d a p", paragraph_example)
1471                .await
1472                .assert_matches();
1473        }
1474    }
1475
1476    #[gpui::test]
1477    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1478        let mut cx = NeovimBackedTestContext::new(cx).await;
1479
1480        const EXAMPLES: &[&str] = &[
1481            indoc! {"
1482                ˇThe quick brown
1483                fox jumps over
1484                the lazy dog.
1485            "},
1486            indoc! {"
1487                ˇ
1488
1489                ˇThe quick brown fox jumps
1490                over the lazy dog.
1491                ˇ
1492
1493                ˇThe quick brown fox jumps
1494                over the lazy dog.
1495            "},
1496            indoc! {"
1497                ˇThe quick brown fox jumps over the lazy dog.
1498                ˇ
1499                ˇThe quick brown fox jumps over the lazy dog.
1500
1501            "},
1502        ];
1503
1504        for paragraph_example in EXAMPLES {
1505            cx.simulate_at_each_offset("v i p", paragraph_example)
1506                .await
1507                .assert_matches();
1508            cx.simulate_at_each_offset("v a p", paragraph_example)
1509                .await
1510                .assert_matches();
1511        }
1512    }
1513
1514    // Test string with "`" for opening surrounders and "'" for closing surrounders
1515    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1516        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1517        'ˇfox juˇmps ov`ˇer
1518        the ˇlazy d'o`ˇg"};
1519
1520    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1521        ('"', '"'), // Double Quote
1522        ('(', ')'), // Parentheses
1523    ];
1524
1525    #[gpui::test]
1526    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1527        let mut cx = NeovimBackedTestContext::new(cx).await;
1528
1529        for (start, end) in SURROUNDING_OBJECTS {
1530            let marked_string = SURROUNDING_MARKER_STRING
1531                .replace('`', &start.to_string())
1532                .replace('\'', &end.to_string());
1533
1534            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1535                .await
1536                .assert_matches();
1537            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1538                .await
1539                .assert_matches();
1540            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1541                .await
1542                .assert_matches();
1543            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1544                .await
1545                .assert_matches();
1546        }
1547    }
1548    #[gpui::test]
1549    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1550        let mut cx = NeovimBackedTestContext::new(cx).await;
1551        cx.set_shared_wrap(12).await;
1552
1553        cx.set_shared_state(indoc! {
1554            "\"ˇhello world\"!"
1555        })
1556        .await;
1557        cx.simulate_shared_keystrokes("v i \"").await;
1558        cx.shared_state().await.assert_eq(indoc! {
1559            "\"«hello worldˇ»\"!"
1560        });
1561
1562        cx.set_shared_state(indoc! {
1563            "\"hˇello world\"!"
1564        })
1565        .await;
1566        cx.simulate_shared_keystrokes("v i \"").await;
1567        cx.shared_state().await.assert_eq(indoc! {
1568            "\"«hello worldˇ»\"!"
1569        });
1570
1571        cx.set_shared_state(indoc! {
1572            "helˇlo \"world\"!"
1573        })
1574        .await;
1575        cx.simulate_shared_keystrokes("v i \"").await;
1576        cx.shared_state().await.assert_eq(indoc! {
1577            "hello \"«worldˇ»\"!"
1578        });
1579
1580        cx.set_shared_state(indoc! {
1581            "hello \"wˇorld\"!"
1582        })
1583        .await;
1584        cx.simulate_shared_keystrokes("v i \"").await;
1585        cx.shared_state().await.assert_eq(indoc! {
1586            "hello \"«worldˇ»\"!"
1587        });
1588
1589        cx.set_shared_state(indoc! {
1590            "hello \"wˇorld\"!"
1591        })
1592        .await;
1593        cx.simulate_shared_keystrokes("v a \"").await;
1594        cx.shared_state().await.assert_eq(indoc! {
1595            "hello« \"world\"ˇ»!"
1596        });
1597
1598        cx.set_shared_state(indoc! {
1599            "hello \"wˇorld\" !"
1600        })
1601        .await;
1602        cx.simulate_shared_keystrokes("v a \"").await;
1603        cx.shared_state().await.assert_eq(indoc! {
1604            "hello «\"world\" ˇ»!"
1605        });
1606
1607        cx.set_shared_state(indoc! {
1608            "hello \"wˇorld\"1609            goodbye"
1610        })
1611        .await;
1612        cx.simulate_shared_keystrokes("v a \"").await;
1613        cx.shared_state().await.assert_eq(indoc! {
1614            "hello «\"world\" ˇ»
1615            goodbye"
1616        });
1617    }
1618
1619    #[gpui::test]
1620    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1621        let mut cx = NeovimBackedTestContext::new(cx).await;
1622
1623        cx.set_shared_state(indoc! {
1624            "func empty(a string) bool {
1625               if a == \"\" {
1626                  return true
1627               }
1628               ˇreturn false
1629            }"
1630        })
1631        .await;
1632        cx.simulate_shared_keystrokes("v i {").await;
1633        cx.shared_state().await.assert_eq(indoc! {"
1634            func empty(a string) bool {
1635            «   if a == \"\" {
1636                  return true
1637               }
1638               return false
1639            ˇ»}"});
1640        cx.set_shared_state(indoc! {
1641            "func empty(a string) bool {
1642                 if a == \"\" {
1643                     ˇreturn true
1644                 }
1645                 return false
1646            }"
1647        })
1648        .await;
1649        cx.simulate_shared_keystrokes("v i {").await;
1650        cx.shared_state().await.assert_eq(indoc! {"
1651            func empty(a string) bool {
1652                 if a == \"\" {
1653            «         return true
1654            ˇ»     }
1655                 return false
1656            }"});
1657
1658        cx.set_shared_state(indoc! {
1659            "func empty(a string) bool {
1660                 if a == \"\" ˇ{
1661                     return true
1662                 }
1663                 return false
1664            }"
1665        })
1666        .await;
1667        cx.simulate_shared_keystrokes("v i {").await;
1668        cx.shared_state().await.assert_eq(indoc! {"
1669            func empty(a string) bool {
1670                 if a == \"\" {
1671            «         return true
1672            ˇ»     }
1673                 return false
1674            }"});
1675    }
1676
1677    #[gpui::test]
1678    async fn test_singleline_surrounding_character_objects_with_escape(
1679        cx: &mut gpui::TestAppContext,
1680    ) {
1681        let mut cx = NeovimBackedTestContext::new(cx).await;
1682        cx.set_shared_state(indoc! {
1683            "h\"e\\\"lˇlo \\\"world\"!"
1684        })
1685        .await;
1686        cx.simulate_shared_keystrokes("v i \"").await;
1687        cx.shared_state().await.assert_eq(indoc! {
1688            "h\"«e\\\"llo \\\"worldˇ»\"!"
1689        });
1690
1691        cx.set_shared_state(indoc! {
1692            "hello \"teˇst \\\"inside\\\" world\""
1693        })
1694        .await;
1695        cx.simulate_shared_keystrokes("v i \"").await;
1696        cx.shared_state().await.assert_eq(indoc! {
1697            "hello \"«test \\\"inside\\\" worldˇ»\""
1698        });
1699    }
1700
1701    #[gpui::test]
1702    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1703        let mut cx = VimTestContext::new(cx, true).await;
1704        cx.set_state(
1705            indoc! {"
1706            fn boop() {
1707                baz(ˇ|a, b| { bar(|j, k| { })})
1708            }"
1709            },
1710            Mode::Normal,
1711        );
1712        cx.simulate_keystrokes("c i |");
1713        cx.assert_state(
1714            indoc! {"
1715            fn boop() {
1716                baz(|ˇ| { bar(|j, k| { })})
1717            }"
1718            },
1719            Mode::Insert,
1720        );
1721        cx.simulate_keystrokes("escape 1 8 |");
1722        cx.assert_state(
1723            indoc! {"
1724            fn boop() {
1725                baz(|| { bar(ˇ|j, k| { })})
1726            }"
1727            },
1728            Mode::Normal,
1729        );
1730
1731        cx.simulate_keystrokes("v a |");
1732        cx.assert_state(
1733            indoc! {"
1734            fn boop() {
1735                baz(|| { bar(«|j, k| ˇ»{ })})
1736            }"
1737            },
1738            Mode::Visual,
1739        );
1740    }
1741
1742    #[gpui::test]
1743    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1744        let mut cx = VimTestContext::new(cx, true).await;
1745
1746        // Generic arguments
1747        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1748        cx.simulate_keystrokes("v i a");
1749        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1750
1751        // Function arguments
1752        cx.set_state(
1753            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1754            Mode::Normal,
1755        );
1756        cx.simulate_keystrokes("d a a");
1757        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1758
1759        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1760        cx.simulate_keystrokes("v a a");
1761        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1762
1763        // Tuple, vec, and array arguments
1764        cx.set_state(
1765            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1766            Mode::Normal,
1767        );
1768        cx.simulate_keystrokes("c i a");
1769        cx.assert_state(
1770            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1771            Mode::Insert,
1772        );
1773
1774        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1775        cx.simulate_keystrokes("c a a");
1776        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1777
1778        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1779        cx.simulate_keystrokes("c i a");
1780        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1781
1782        cx.set_state(
1783            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1784            Mode::Normal,
1785        );
1786        cx.simulate_keystrokes("c a a");
1787        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1788
1789        // Cursor immediately before / after brackets
1790        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1791        cx.simulate_keystrokes("v i a");
1792        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1793
1794        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1795        cx.simulate_keystrokes("v i a");
1796        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1797    }
1798
1799    #[gpui::test]
1800    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1801        let mut cx = VimTestContext::new(cx, true).await;
1802
1803        // Base use case
1804        cx.set_state(
1805            indoc! {"
1806                fn boop() {
1807                    // Comment
1808                    baz();ˇ
1809
1810                    loop {
1811                        bar(1);
1812                        bar(2);
1813                    }
1814
1815                    result
1816                }
1817            "},
1818            Mode::Normal,
1819        );
1820        cx.simulate_keystrokes("v i i");
1821        cx.assert_state(
1822            indoc! {"
1823                fn boop() {
1824                «    // Comment
1825                    baz();
1826
1827                    loop {
1828                        bar(1);
1829                        bar(2);
1830                    }
1831
1832                    resultˇ»
1833                }
1834            "},
1835            Mode::Visual,
1836        );
1837
1838        // Around indent (include line above)
1839        cx.set_state(
1840            indoc! {"
1841                const ABOVE: str = true;
1842                fn boop() {
1843
1844                    hello();
1845                    worˇld()
1846                }
1847            "},
1848            Mode::Normal,
1849        );
1850        cx.simulate_keystrokes("v a i");
1851        cx.assert_state(
1852            indoc! {"
1853                const ABOVE: str = true;
1854                «fn boop() {
1855
1856                    hello();
1857                    world()ˇ»
1858                }
1859            "},
1860            Mode::Visual,
1861        );
1862
1863        // Around indent (include line above & below)
1864        cx.set_state(
1865            indoc! {"
1866                const ABOVE: str = true;
1867                fn boop() {
1868                    hellˇo();
1869                    world()
1870
1871                }
1872                const BELOW: str = true;
1873            "},
1874            Mode::Normal,
1875        );
1876        cx.simulate_keystrokes("c a shift-i");
1877        cx.assert_state(
1878            indoc! {"
1879                const ABOVE: str = true;
1880                ˇ
1881                const BELOW: str = true;
1882            "},
1883            Mode::Insert,
1884        );
1885    }
1886
1887    #[gpui::test]
1888    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1889        let mut cx = NeovimBackedTestContext::new(cx).await;
1890
1891        for (start, end) in SURROUNDING_OBJECTS {
1892            let marked_string = SURROUNDING_MARKER_STRING
1893                .replace('`', &start.to_string())
1894                .replace('\'', &end.to_string());
1895
1896            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1897                .await
1898                .assert_matches();
1899            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1900                .await
1901                .assert_matches();
1902            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1903                .await
1904                .assert_matches();
1905            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1906                .await
1907                .assert_matches();
1908        }
1909    }
1910
1911    #[gpui::test]
1912    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
1913        let mut cx = VimTestContext::new(cx, true).await;
1914
1915        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
1916            // Single quotes
1917            (
1918                "c i q",
1919                "This is a 'qˇuote' example.",
1920                "This is a 'ˇ' example.",
1921                Mode::Insert,
1922            ),
1923            (
1924                "c a q",
1925                "This is a 'qˇuote' example.",
1926                "This is a ˇexample.",
1927                Mode::Insert,
1928            ),
1929            (
1930                "d i q",
1931                "This is a 'qˇuote' example.",
1932                "This is a 'ˇ' example.",
1933                Mode::Normal,
1934            ),
1935            (
1936                "d a q",
1937                "This is a 'qˇuote' example.",
1938                "This is a ˇexample.",
1939                Mode::Normal,
1940            ),
1941            // Double quotes
1942            (
1943                "c i q",
1944                "This is a \"qˇuote\" example.",
1945                "This is a \"ˇ\" example.",
1946                Mode::Insert,
1947            ),
1948            (
1949                "c a q",
1950                "This is a \"qˇuote\" example.",
1951                "This is a ˇexample.",
1952                Mode::Insert,
1953            ),
1954            (
1955                "d i q",
1956                "This is a \"qˇuote\" example.",
1957                "This is a \"ˇ\" example.",
1958                Mode::Normal,
1959            ),
1960            (
1961                "d a q",
1962                "This is a \"qˇuote\" example.",
1963                "This is a ˇexample.",
1964                Mode::Normal,
1965            ),
1966            // Back quotes
1967            (
1968                "c i q",
1969                "This is a `qˇuote` example.",
1970                "This is a `ˇ` example.",
1971                Mode::Insert,
1972            ),
1973            (
1974                "c a q",
1975                "This is a `qˇuote` example.",
1976                "This is a ˇexample.",
1977                Mode::Insert,
1978            ),
1979            (
1980                "d i q",
1981                "This is a `qˇuote` example.",
1982                "This is a `ˇ` example.",
1983                Mode::Normal,
1984            ),
1985            (
1986                "d a q",
1987                "This is a `qˇuote` example.",
1988                "This is a ˇexample.",
1989                Mode::Normal,
1990            ),
1991        ];
1992
1993        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
1994            cx.set_state(initial_state, Mode::Normal);
1995
1996            cx.simulate_keystrokes(keystrokes);
1997
1998            cx.assert_state(expected_state, *expected_mode);
1999        }
2000
2001        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2002            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2003            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2004            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2005            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2006            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2007            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2008            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2009            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2010            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2011            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2012            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2013            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2014        ];
2015
2016        for (keystrokes, initial_state, mode) in INVALID_CASES {
2017            cx.set_state(initial_state, Mode::Normal);
2018
2019            cx.simulate_keystrokes(keystrokes);
2020
2021            cx.assert_state(initial_state, *mode);
2022        }
2023    }
2024
2025    #[gpui::test]
2026    async fn test_tags(cx: &mut gpui::TestAppContext) {
2027        let mut cx = VimTestContext::new_html(cx).await;
2028
2029        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
2030        cx.simulate_keystrokes("v i t");
2031        cx.assert_state(
2032            "<html><head></head><body><b>«hi!ˇ»</b></body>",
2033            Mode::Visual,
2034        );
2035        cx.simulate_keystrokes("a t");
2036        cx.assert_state(
2037            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2038            Mode::Visual,
2039        );
2040        cx.simulate_keystrokes("a t");
2041        cx.assert_state(
2042            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2043            Mode::Visual,
2044        );
2045
2046        // The cursor is before the tag
2047        cx.set_state(
2048            "<html><head></head><body> ˇ  <b>hi!</b></body>",
2049            Mode::Normal,
2050        );
2051        cx.simulate_keystrokes("v i t");
2052        cx.assert_state(
2053            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
2054            Mode::Visual,
2055        );
2056        cx.simulate_keystrokes("a t");
2057        cx.assert_state(
2058            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
2059            Mode::Visual,
2060        );
2061
2062        // The cursor is in the open tag
2063        cx.set_state(
2064            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
2065            Mode::Normal,
2066        );
2067        cx.simulate_keystrokes("v a t");
2068        cx.assert_state(
2069            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
2070            Mode::Visual,
2071        );
2072        cx.simulate_keystrokes("i t");
2073        cx.assert_state(
2074            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
2075            Mode::Visual,
2076        );
2077
2078        // current selection length greater than 1
2079        cx.set_state(
2080            "<html><head></head><body><«b>hi!ˇ»</b></body>",
2081            Mode::Visual,
2082        );
2083        cx.simulate_keystrokes("i t");
2084        cx.assert_state(
2085            "<html><head></head><body><b>«hi!ˇ»</b></body>",
2086            Mode::Visual,
2087        );
2088        cx.simulate_keystrokes("a t");
2089        cx.assert_state(
2090            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2091            Mode::Visual,
2092        );
2093
2094        cx.set_state(
2095            "<html><head></head><body><«b>hi!</ˇ»b></body>",
2096            Mode::Visual,
2097        );
2098        cx.simulate_keystrokes("a t");
2099        cx.assert_state(
2100            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2101            Mode::Visual,
2102        );
2103    }
2104}