helix.rs

   1mod boundary;
   2mod duplicate;
   3mod object;
   4mod paste;
   5mod select;
   6mod surround;
   7
   8use editor::display_map::DisplaySnapshot;
   9use editor::{
  10    DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
  11    SelectionEffects, ToOffset, ToPoint, movement,
  12};
  13use gpui::actions;
  14use gpui::{Context, Window};
  15use language::{CharClassifier, CharKind, Point};
  16use search::{BufferSearchBar, SearchOptions};
  17use settings::Settings;
  18use text::{Bias, SelectionGoal};
  19use workspace::searchable::FilteredSearchRange;
  20use workspace::searchable::{self, Direction};
  21
  22use crate::motion::{self, MotionKind};
  23use crate::state::{Operator, SearchState};
  24use crate::{
  25    PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim,
  26    motion::{Motion, right},
  27    state::Mode,
  28};
  29
  30actions!(
  31    vim,
  32    [
  33        /// Yanks the current selection or character if no selection.
  34        HelixYank,
  35        /// Inserts at the beginning of the selection.
  36        HelixInsert,
  37        /// Appends at the end of the selection.
  38        HelixAppend,
  39        /// Inserts at the end of the current Helix cursor line.
  40        HelixInsertEndOfLine,
  41        /// Goes to the location of the last modification.
  42        HelixGotoLastModification,
  43        /// Select entire line or multiple lines, extending downwards.
  44        HelixSelectLine,
  45        /// Select all matches of a given pattern within the current selection.
  46        HelixSelectRegex,
  47        /// Removes all but the one selection that was created last.
  48        /// `Newest` can eventually be `Primary`.
  49        HelixKeepNewestSelection,
  50        /// Copies all selections below.
  51        HelixDuplicateBelow,
  52        /// Copies all selections above.
  53        HelixDuplicateAbove,
  54        /// Delete the selection and enter edit mode.
  55        HelixSubstitute,
  56        /// Delete the selection and enter edit mode, without yanking the selection.
  57        HelixSubstituteNoYank,
  58        /// Select the next match for the current search query.
  59        HelixSelectNext,
  60        /// Select the previous match for the current search query.
  61        HelixSelectPrevious,
  62    ]
  63);
  64
  65pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  66    Vim::action(editor, cx, Vim::helix_select_lines);
  67    Vim::action(editor, cx, Vim::helix_insert);
  68    Vim::action(editor, cx, Vim::helix_append);
  69    Vim::action(editor, cx, Vim::helix_insert_end_of_line);
  70    Vim::action(editor, cx, Vim::helix_yank);
  71    Vim::action(editor, cx, Vim::helix_goto_last_modification);
  72    Vim::action(editor, cx, Vim::helix_paste);
  73    Vim::action(editor, cx, Vim::helix_select_regex);
  74    Vim::action(editor, cx, Vim::helix_keep_newest_selection);
  75    Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
  76        let times = Vim::take_count(cx);
  77        vim.helix_duplicate_selections_below(times, window, cx);
  78    });
  79    Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
  80        let times = Vim::take_count(cx);
  81        vim.helix_duplicate_selections_above(times, window, cx);
  82    });
  83    Vim::action(editor, cx, Vim::helix_substitute);
  84    Vim::action(editor, cx, Vim::helix_substitute_no_yank);
  85    Vim::action(editor, cx, Vim::helix_select_next);
  86    Vim::action(editor, cx, Vim::helix_select_previous);
  87    Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
  88        vim.clear_operator(window, cx);
  89        vim.push_operator(Operator::HelixSurroundAdd, window, cx);
  90    });
  91    Vim::action(
  92        editor,
  93        cx,
  94        |vim, _: &PushHelixSurroundReplace, window, cx| {
  95            vim.clear_operator(window, cx);
  96            vim.push_operator(
  97                Operator::HelixSurroundReplace {
  98                    replaced_char: None,
  99                },
 100                window,
 101                cx,
 102            );
 103        },
 104    );
 105    Vim::action(
 106        editor,
 107        cx,
 108        |vim, _: &PushHelixSurroundDelete, window, cx| {
 109            vim.clear_operator(window, cx);
 110            vim.push_operator(Operator::HelixSurroundDelete, window, cx);
 111        },
 112    );
 113}
 114
 115impl Vim {
 116    pub fn helix_normal_motion(
 117        &mut self,
 118        motion: Motion,
 119        times: Option<usize>,
 120        window: &mut Window,
 121        cx: &mut Context<Self>,
 122    ) {
 123        self.helix_move_cursor(motion, times, window, cx);
 124    }
 125
 126    pub fn helix_select_motion(
 127        &mut self,
 128        motion: Motion,
 129        times: Option<usize>,
 130        window: &mut Window,
 131        cx: &mut Context<Self>,
 132    ) {
 133        self.update_editor(cx, |_, editor, cx| {
 134            let text_layout_details = editor.text_layout_details(window, cx);
 135            editor.change_selections(Default::default(), window, cx, |s| {
 136                if let Motion::ZedSearchResult { new_selections, .. } = &motion {
 137                    s.select_anchor_ranges(new_selections.clone());
 138                    return;
 139                };
 140
 141                s.move_with(&mut |map, selection| {
 142                    let was_reversed = selection.reversed;
 143                    let mut current_head = selection.head();
 144
 145                    // our motions assume the current character is after the cursor,
 146                    // but in (forward) visual mode the current character is just
 147                    // before the end of the selection.
 148
 149                    // If the file ends with a newline (which is common) we don't do this.
 150                    // so that if you go to the end of such a file you can use "up" to go
 151                    // to the previous line and have it work somewhat as expected.
 152                    if !selection.reversed
 153                        && !selection.is_empty()
 154                        && !(selection.end.column() == 0 && selection.end == map.max_point())
 155                    {
 156                        current_head = movement::left(map, selection.end)
 157                    }
 158
 159                    let (new_head, goal) = match motion {
 160                        // EndOfLine positions after the last character, but in
 161                        // helix visual mode we want the selection to end ON the
 162                        // last character. Adjust left here so the subsequent
 163                        // right-expansion (below) includes the last char without
 164                        // spilling into the newline.
 165                        Motion::EndOfLine { .. } => {
 166                            let (point, goal) = motion
 167                                .move_point(
 168                                    map,
 169                                    current_head,
 170                                    selection.goal,
 171                                    times,
 172                                    &text_layout_details,
 173                                )
 174                                .unwrap_or((current_head, selection.goal));
 175                            (movement::saturating_left(map, point), goal)
 176                        }
 177                        // Going to next word start is special cased
 178                        // since Vim differs from Helix in that motion
 179                        // Vim: `w` goes to the first character of a word
 180                        // Helix: `w` goes to the character before a word
 181                        Motion::NextWordStart { ignore_punctuation } => {
 182                            let mut head = movement::right(map, current_head);
 183                            let classifier =
 184                                map.buffer_snapshot().char_classifier_at(head.to_point(map));
 185                            for _ in 0..times.unwrap_or(1) {
 186                                let (_, new_head) =
 187                                    movement::find_boundary_trail(map, head, &mut |left, right| {
 188                                        Self::is_boundary_right(ignore_punctuation)(
 189                                            left,
 190                                            right,
 191                                            &classifier,
 192                                        )
 193                                    });
 194                                head = new_head;
 195                            }
 196                            head = movement::left(map, head);
 197                            (head, SelectionGoal::None)
 198                        }
 199                        _ => motion
 200                            .move_point(
 201                                map,
 202                                current_head,
 203                                selection.goal,
 204                                times,
 205                                &text_layout_details,
 206                            )
 207                            .unwrap_or((current_head, selection.goal)),
 208                    };
 209
 210                    selection.set_head(new_head, goal);
 211
 212                    // ensure the current character is included in the selection.
 213                    if !selection.reversed {
 214                        let next_point = movement::right(map, selection.end);
 215
 216                        if !(next_point.column() == 0 && next_point == map.max_point()) {
 217                            selection.end = next_point;
 218                        }
 219                    }
 220
 221                    // vim always ensures the anchor character stays selected.
 222                    // if our selection has reversed, we need to move the opposite end
 223                    // to ensure the anchor is still selected.
 224                    if was_reversed && !selection.reversed {
 225                        selection.start = movement::left(map, selection.start);
 226                    } else if !was_reversed && selection.reversed {
 227                        selection.end = movement::right(map, selection.end);
 228                    }
 229                })
 230            });
 231        });
 232    }
 233
 234    /// Updates all selections based on where the cursors are.
 235    fn helix_new_selections(
 236        &mut self,
 237        window: &mut Window,
 238        cx: &mut Context<Self>,
 239        change: &mut dyn FnMut(
 240            // the start of the cursor
 241            DisplayPoint,
 242            &DisplaySnapshot,
 243        ) -> Option<(DisplayPoint, DisplayPoint)>,
 244    ) {
 245        self.update_editor(cx, |_, editor, cx| {
 246            editor.change_selections(Default::default(), window, cx, |s| {
 247                s.move_with(&mut |map, selection| {
 248                    let cursor_start = if selection.reversed || selection.is_empty() {
 249                        selection.head()
 250                    } else {
 251                        movement::left(map, selection.head())
 252                    };
 253                    let Some((head, tail)) = change(cursor_start, map) else {
 254                        return;
 255                    };
 256
 257                    selection.set_head_tail(head, tail, SelectionGoal::None);
 258                });
 259            });
 260        });
 261    }
 262
 263    fn helix_find_range_forward(
 264        &mut self,
 265        times: Option<usize>,
 266        window: &mut Window,
 267        cx: &mut Context<Self>,
 268        is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
 269    ) {
 270        let times = times.unwrap_or(1);
 271        self.helix_new_selections(window, cx, &mut |cursor, map| {
 272            let mut head = movement::right(map, cursor);
 273            let mut tail = cursor;
 274            let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
 275            if head == map.max_point() {
 276                return None;
 277            }
 278            for _ in 0..times {
 279                let (maybe_next_tail, next_head) =
 280                    movement::find_boundary_trail(map, head, &mut |left, right| {
 281                        is_boundary(left, right, &classifier)
 282                    });
 283
 284                if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
 285                    break;
 286                }
 287
 288                head = next_head;
 289                if let Some(next_tail) = maybe_next_tail {
 290                    tail = next_tail;
 291                }
 292            }
 293            Some((head, tail))
 294        });
 295    }
 296
 297    fn helix_find_range_backward(
 298        &mut self,
 299        times: Option<usize>,
 300        window: &mut Window,
 301        cx: &mut Context<Self>,
 302        is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
 303    ) {
 304        let times = times.unwrap_or(1);
 305        self.helix_new_selections(window, cx, &mut |cursor, map| {
 306            let mut head = cursor;
 307            // The original cursor was one character wide,
 308            // but the search starts from the left side of it,
 309            // so to include that space the selection must end one character to the right.
 310            let mut tail = movement::right(map, cursor);
 311            let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
 312            if head == DisplayPoint::zero() {
 313                return None;
 314            }
 315            for _ in 0..times {
 316                let (maybe_next_tail, next_head) =
 317                    movement::find_preceding_boundary_trail(map, head, &mut |left, right| {
 318                        is_boundary(left, right, &classifier)
 319                    });
 320
 321                if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
 322                    break;
 323                }
 324
 325                head = next_head;
 326                if let Some(next_tail) = maybe_next_tail {
 327                    tail = next_tail;
 328                }
 329            }
 330            Some((head, tail))
 331        });
 332    }
 333
 334    pub fn helix_move_and_collapse(
 335        &mut self,
 336        motion: Motion,
 337        times: Option<usize>,
 338        window: &mut Window,
 339        cx: &mut Context<Self>,
 340    ) {
 341        self.update_editor(cx, |_, editor, cx| {
 342            let text_layout_details = editor.text_layout_details(window, cx);
 343            editor.change_selections(Default::default(), window, cx, |s| {
 344                s.move_with(&mut |map, selection| {
 345                    let goal = selection.goal;
 346                    let cursor = if selection.is_empty() || selection.reversed {
 347                        selection.head()
 348                    } else {
 349                        movement::left(map, selection.head())
 350                    };
 351
 352                    let (point, goal) = motion
 353                        .move_point(map, cursor, selection.goal, times, &text_layout_details)
 354                        .unwrap_or((cursor, goal));
 355
 356                    selection.collapse_to(point, goal)
 357                })
 358            });
 359        });
 360    }
 361
 362    fn is_boundary_right(
 363        ignore_punctuation: bool,
 364    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
 365        move |left, right, classifier| {
 366            let left_kind = classifier.kind_with(left, ignore_punctuation);
 367            let right_kind = classifier.kind_with(right, ignore_punctuation);
 368            let at_newline = (left == '\n') ^ (right == '\n');
 369
 370            (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
 371        }
 372    }
 373
 374    fn is_boundary_left(
 375        ignore_punctuation: bool,
 376    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
 377        move |left, right, classifier| {
 378            let left_kind = classifier.kind_with(left, ignore_punctuation);
 379            let right_kind = classifier.kind_with(right, ignore_punctuation);
 380            let at_newline = (left == '\n') ^ (right == '\n');
 381
 382            (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
 383        }
 384    }
 385
 386    /// When `reversed` is true (used with `helix_find_range_backward`), the
 387    /// `left` and `right` characters are yielded in reverse text order, so the
 388    /// camelCase transition check must be flipped accordingly.
 389    fn subword_boundary_start(
 390        ignore_punctuation: bool,
 391        reversed: bool,
 392    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
 393        move |left, right, classifier| {
 394            let left_kind = classifier.kind_with(left, ignore_punctuation);
 395            let right_kind = classifier.kind_with(right, ignore_punctuation);
 396            let at_newline = (left == '\n') ^ (right == '\n');
 397            let is_separator = |c: char| "_$=".contains(c);
 398
 399            let is_word = left_kind != right_kind && right_kind != CharKind::Whitespace;
 400            let is_subword = (is_separator(left) && !is_separator(right))
 401                || if reversed {
 402                    right.is_lowercase() && left.is_uppercase()
 403                } else {
 404                    left.is_lowercase() && right.is_uppercase()
 405                };
 406
 407            is_word || (is_subword && !right.is_whitespace()) || at_newline
 408        }
 409    }
 410
 411    /// When `reversed` is true (used with `helix_find_range_backward`), the
 412    /// `left` and `right` characters are yielded in reverse text order, so the
 413    /// camelCase transition check must be flipped accordingly.
 414    fn subword_boundary_end(
 415        ignore_punctuation: bool,
 416        reversed: bool,
 417    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
 418        move |left, right, classifier| {
 419            let left_kind = classifier.kind_with(left, ignore_punctuation);
 420            let right_kind = classifier.kind_with(right, ignore_punctuation);
 421            let at_newline = (left == '\n') ^ (right == '\n');
 422            let is_separator = |c: char| "_$=".contains(c);
 423
 424            let is_word = left_kind != right_kind && left_kind != CharKind::Whitespace;
 425            let is_subword = (!is_separator(left) && is_separator(right))
 426                || if reversed {
 427                    right.is_lowercase() && left.is_uppercase()
 428                } else {
 429                    left.is_lowercase() && right.is_uppercase()
 430                };
 431
 432            is_word || (is_subword && !left.is_whitespace()) || at_newline
 433        }
 434    }
 435
 436    pub fn helix_move_cursor(
 437        &mut self,
 438        motion: Motion,
 439        times: Option<usize>,
 440        window: &mut Window,
 441        cx: &mut Context<Self>,
 442    ) {
 443        match motion {
 444            Motion::NextWordStart { ignore_punctuation } => {
 445                let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
 446                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
 447            }
 448            Motion::NextWordEnd { ignore_punctuation } => {
 449                let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
 450                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
 451            }
 452            Motion::PreviousWordStart { ignore_punctuation } => {
 453                let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
 454                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
 455            }
 456            Motion::PreviousWordEnd { ignore_punctuation } => {
 457                let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
 458                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
 459            }
 460            // The subword motions implementation is based off of the same
 461            // commands present in Helix itself, namely:
 462            //
 463            // * `move_next_sub_word_start`
 464            // * `move_next_sub_word_end`
 465            // * `move_prev_sub_word_start`
 466            // * `move_prev_sub_word_end`
 467            Motion::NextSubwordStart { ignore_punctuation } => {
 468                let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, false);
 469                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
 470            }
 471            Motion::NextSubwordEnd { ignore_punctuation } => {
 472                let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, false);
 473                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
 474            }
 475            Motion::PreviousSubwordStart { ignore_punctuation } => {
 476                let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, true);
 477                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
 478            }
 479            Motion::PreviousSubwordEnd { ignore_punctuation } => {
 480                let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, true);
 481                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
 482            }
 483            Motion::EndOfLine { .. } => {
 484                // In Helix mode, EndOfLine should position cursor ON the last character,
 485                // not after it. We therefore need special handling for it.
 486                self.update_editor(cx, |_, editor, cx| {
 487                    let text_layout_details = editor.text_layout_details(window, cx);
 488                    editor.change_selections(Default::default(), window, cx, |s| {
 489                        s.move_with(&mut |map, selection| {
 490                            let goal = selection.goal;
 491                            let cursor = if selection.is_empty() || selection.reversed {
 492                                selection.head()
 493                            } else {
 494                                movement::left(map, selection.head())
 495                            };
 496
 497                            let (point, _goal) = motion
 498                                .move_point(map, cursor, goal, times, &text_layout_details)
 499                                .unwrap_or((cursor, goal));
 500
 501                            // Move left by one character to position on the last character
 502                            let adjusted_point = movement::saturating_left(map, point);
 503                            selection.collapse_to(adjusted_point, SelectionGoal::None)
 504                        })
 505                    });
 506                });
 507            }
 508            Motion::FindForward {
 509                before,
 510                char,
 511                mode,
 512                smartcase,
 513            } => {
 514                self.helix_new_selections(window, cx, &mut |cursor, map| {
 515                    let start = cursor;
 516                    let mut last_boundary = start;
 517                    for _ in 0..times.unwrap_or(1) {
 518                        last_boundary = movement::find_boundary(
 519                            map,
 520                            movement::right(map, last_boundary),
 521                            mode,
 522                            &mut |left, right| {
 523                                let current_char = if before { right } else { left };
 524                                motion::is_character_match(char, current_char, smartcase)
 525                            },
 526                        );
 527                    }
 528                    Some((last_boundary, start))
 529                });
 530            }
 531            Motion::FindBackward {
 532                after,
 533                char,
 534                mode,
 535                smartcase,
 536            } => {
 537                self.helix_new_selections(window, cx, &mut |cursor, map| {
 538                    let start = cursor;
 539                    let mut last_boundary = start;
 540                    for _ in 0..times.unwrap_or(1) {
 541                        last_boundary = movement::find_preceding_boundary_display_point(
 542                            map,
 543                            last_boundary,
 544                            mode,
 545                            &mut |left, right| {
 546                                let current_char = if after { left } else { right };
 547                                motion::is_character_match(char, current_char, smartcase)
 548                            },
 549                        );
 550                    }
 551                    // The original cursor was one character wide,
 552                    // but the search started from the left side of it,
 553                    // so to include that space the selection must end one character to the right.
 554                    Some((last_boundary, movement::right(map, start)))
 555                });
 556            }
 557            _ => self.helix_move_and_collapse(motion, times, window, cx),
 558        }
 559    }
 560
 561    pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
 562        self.update_editor(cx, |vim, editor, cx| {
 563            let has_selection = editor
 564                .selections
 565                .all_adjusted(&editor.display_snapshot(cx))
 566                .iter()
 567                .any(|selection| !selection.is_empty());
 568
 569            if !has_selection {
 570                // If no selection, expand to current character (like 'v' does)
 571                editor.change_selections(Default::default(), window, cx, |s| {
 572                    s.move_with(&mut |map, selection| {
 573                        let head = selection.head();
 574                        let new_head = movement::saturating_right(map, head);
 575                        selection.set_tail(head, SelectionGoal::None);
 576                        selection.set_head(new_head, SelectionGoal::None);
 577                    });
 578                });
 579                vim.yank_selections_content(
 580                    editor,
 581                    crate::motion::MotionKind::Exclusive,
 582                    window,
 583                    cx,
 584                );
 585                editor.change_selections(Default::default(), window, cx, |s| {
 586                    s.move_with(&mut |_map, selection| {
 587                        selection.collapse_to(selection.start, SelectionGoal::None);
 588                    });
 589                });
 590            } else {
 591                // Yank the selection(s)
 592                vim.yank_selections_content(
 593                    editor,
 594                    crate::motion::MotionKind::Exclusive,
 595                    window,
 596                    cx,
 597                );
 598            }
 599        });
 600
 601        // Drop back to normal mode after yanking
 602        self.switch_mode(Mode::HelixNormal, true, window, cx);
 603    }
 604
 605    fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
 606        self.start_recording(cx);
 607        self.update_editor(cx, |_, editor, cx| {
 608            editor.change_selections(Default::default(), window, cx, |s| {
 609                s.move_with(&mut |_map, selection| {
 610                    // In helix normal mode, move cursor to start of selection and collapse
 611                    if !selection.is_empty() {
 612                        selection.collapse_to(selection.start, SelectionGoal::None);
 613                    }
 614                });
 615            });
 616        });
 617        self.switch_mode(Mode::Insert, false, window, cx);
 618    }
 619
 620    fn helix_select_regex(
 621        &mut self,
 622        _: &HelixSelectRegex,
 623        window: &mut Window,
 624        cx: &mut Context<Self>,
 625    ) {
 626        Vim::take_forced_motion(cx);
 627        let Some(pane) = self.pane(window, cx) else {
 628            return;
 629        };
 630        let prior_selections = self.editor_selections(window, cx);
 631        pane.update(cx, |pane, cx| {
 632            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 633                search_bar.update(cx, |search_bar, cx| {
 634                    if !search_bar.show(window, cx) {
 635                        return;
 636                    }
 637
 638                    search_bar.select_query(window, cx);
 639                    cx.focus_self(window);
 640
 641                    search_bar.set_replacement(None, cx);
 642                    let mut options = SearchOptions::NONE;
 643                    options |= SearchOptions::REGEX;
 644                    if EditorSettings::get_global(cx).search.case_sensitive {
 645                        options |= SearchOptions::CASE_SENSITIVE;
 646                    }
 647                    search_bar.set_search_options(options, cx);
 648                    if let Some(search) = search_bar.set_search_within_selection(
 649                        Some(FilteredSearchRange::Selection),
 650                        window,
 651                        cx,
 652                    ) {
 653                        cx.spawn_in(window, async move |search_bar, cx| {
 654                            if search.await.is_ok() {
 655                                search_bar.update_in(cx, |search_bar, window, cx| {
 656                                    search_bar.activate_current_match(window, cx)
 657                                })
 658                            } else {
 659                                Ok(())
 660                            }
 661                        })
 662                        .detach_and_log_err(cx);
 663                    }
 664                    self.search = SearchState {
 665                        direction: searchable::Direction::Next,
 666                        count: 1,
 667                        cmd_f_search: false,
 668                        prior_selections,
 669                        prior_operator: self.operator_stack.last().cloned(),
 670                        prior_mode: self.mode,
 671                        helix_select: true,
 672                        _dismiss_subscription: None,
 673                    }
 674                });
 675            }
 676        });
 677        self.start_recording(cx);
 678    }
 679
 680    fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
 681        self.start_recording(cx);
 682        self.switch_mode(Mode::Insert, false, window, cx);
 683        self.update_editor(cx, |_, editor, cx| {
 684            editor.change_selections(Default::default(), window, cx, |s| {
 685                s.move_with(&mut |map, selection| {
 686                    let point = if selection.is_empty() {
 687                        right(map, selection.head(), 1)
 688                    } else {
 689                        selection.end
 690                    };
 691                    selection.collapse_to(point, SelectionGoal::None);
 692                });
 693            });
 694        });
 695    }
 696
 697    /// Helix-specific implementation of `shift-a` that accounts for Helix's
 698    /// selection model, where selecting a line with `x` creates a selection
 699    /// from column 0 of the current row to column 0 of the next row, so the
 700    /// default [`vim::normal::InsertEndOfLine`] would move the cursor to the
 701    /// end of the wrong line.
 702    fn helix_insert_end_of_line(
 703        &mut self,
 704        _: &HelixInsertEndOfLine,
 705        window: &mut Window,
 706        cx: &mut Context<Self>,
 707    ) {
 708        self.start_recording(cx);
 709        self.switch_mode(Mode::Insert, false, window, cx);
 710        self.update_editor(cx, |_, editor, cx| {
 711            editor.change_selections(Default::default(), window, cx, |s| {
 712                s.move_with(&mut |map, selection| {
 713                    let cursor = if !selection.is_empty() && !selection.reversed {
 714                        movement::left(map, selection.head())
 715                    } else {
 716                        selection.head()
 717                    };
 718                    selection
 719                        .collapse_to(motion::next_line_end(map, cursor, 1), SelectionGoal::None);
 720                });
 721            });
 722        });
 723    }
 724
 725    pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
 726        self.update_editor(cx, |_, editor, cx| {
 727            editor.transact(window, cx, |editor, window, cx| {
 728                let display_map = editor.display_snapshot(cx);
 729                let selections = editor.selections.all_display(&display_map);
 730
 731                let mut edits = Vec::new();
 732                let mut selection_info = Vec::new();
 733                for selection in &selections {
 734                    let mut range = selection.range();
 735                    let was_empty = range.is_empty();
 736                    let was_reversed = selection.reversed;
 737
 738                    if was_empty {
 739                        range.end = movement::saturating_right(&display_map, range.start);
 740                    }
 741
 742                    let byte_range = range.start.to_offset(&display_map, Bias::Left)
 743                        ..range.end.to_offset(&display_map, Bias::Left);
 744
 745                    let snapshot = display_map.buffer_snapshot();
 746                    let grapheme_count = snapshot.grapheme_count_for_range(&byte_range);
 747                    let anchor = snapshot.anchor_before(byte_range.start);
 748
 749                    selection_info.push((anchor, grapheme_count, was_empty, was_reversed));
 750
 751                    if !byte_range.is_empty() {
 752                        let replacement_text = text.repeat(grapheme_count);
 753                        edits.push((byte_range, replacement_text));
 754                    }
 755                }
 756
 757                editor.edit(edits, cx);
 758
 759                // Restore selections based on original info
 760                let snapshot = editor.buffer().read(cx).snapshot(cx);
 761                let ranges: Vec<_> = selection_info
 762                    .into_iter()
 763                    .map(|(start_anchor, grapheme_count, was_empty, was_reversed)| {
 764                        let start_point = start_anchor.to_point(&snapshot);
 765                        if was_empty {
 766                            start_point..start_point
 767                        } else {
 768                            let replacement_len = text.len() * grapheme_count;
 769                            let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
 770                            let end_point = snapshot.offset_to_point(end_offset);
 771                            if was_reversed {
 772                                end_point..start_point
 773                            } else {
 774                                start_point..end_point
 775                            }
 776                        }
 777                    })
 778                    .collect();
 779
 780                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 781                    s.select_ranges(ranges);
 782                });
 783            });
 784        });
 785        self.switch_mode(Mode::HelixNormal, true, window, cx);
 786    }
 787
 788    pub fn helix_goto_last_modification(
 789        &mut self,
 790        _: &HelixGotoLastModification,
 791        window: &mut Window,
 792        cx: &mut Context<Self>,
 793    ) {
 794        self.jump(".".into(), false, false, window, cx);
 795    }
 796
 797    pub fn helix_select_lines(
 798        &mut self,
 799        _: &HelixSelectLine,
 800        window: &mut Window,
 801        cx: &mut Context<Self>,
 802    ) {
 803        let count = Vim::take_count(cx).unwrap_or(1);
 804        self.update_editor(cx, |_, editor, cx| {
 805            editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 806            let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
 807            let mut selections = editor.selections.all::<Point>(&display_map);
 808            let max_point = display_map.buffer_snapshot().max_point();
 809            let buffer_snapshot = &display_map.buffer_snapshot();
 810
 811            for selection in &mut selections {
 812                // Start always goes to column 0 of the first selected line
 813                let start_row = selection.start.row;
 814                let current_end_row = selection.end.row;
 815
 816                // Check if cursor is on empty line by checking first character
 817                let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
 818                let first_char = buffer_snapshot.chars_at(line_start_offset).next();
 819                let extra_line = if first_char == Some('\n') && selection.is_empty() {
 820                    1
 821                } else {
 822                    0
 823                };
 824
 825                let end_row = current_end_row + count as u32 + extra_line;
 826
 827                selection.start = Point::new(start_row, 0);
 828                selection.end = if end_row > max_point.row {
 829                    max_point
 830                } else {
 831                    Point::new(end_row, 0)
 832                };
 833                selection.reversed = false;
 834            }
 835
 836            editor.change_selections(Default::default(), window, cx, |s| {
 837                s.select(selections);
 838            });
 839        });
 840    }
 841
 842    fn helix_keep_newest_selection(
 843        &mut self,
 844        _: &HelixKeepNewestSelection,
 845        window: &mut Window,
 846        cx: &mut Context<Self>,
 847    ) {
 848        self.update_editor(cx, |_, editor, cx| {
 849            let newest = editor
 850                .selections
 851                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
 852            editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
 853        });
 854    }
 855
 856    fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
 857        self.update_editor(cx, |vim, editor, cx| {
 858            editor.set_clip_at_line_ends(false, cx);
 859            editor.transact(window, cx, |editor, window, cx| {
 860                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 861                    s.move_with(&mut |map, selection| {
 862                        if selection.start == selection.end {
 863                            selection.end = movement::right(map, selection.end);
 864                        }
 865
 866                        // If the selection starts and ends on a newline, we exclude the last one.
 867                        if !selection.is_empty()
 868                            && selection.start.column() == 0
 869                            && selection.end.column() == 0
 870                        {
 871                            selection.end = movement::left(map, selection.end);
 872                        }
 873                    })
 874                });
 875                if yank {
 876                    vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
 877                }
 878                let selections = editor
 879                    .selections
 880                    .all::<Point>(&editor.display_snapshot(cx))
 881                    .into_iter();
 882                let edits = selections.map(|selection| (selection.start..selection.end, ""));
 883                editor.edit(edits, cx);
 884            });
 885        });
 886        self.switch_mode(Mode::Insert, true, window, cx);
 887    }
 888
 889    fn helix_substitute(
 890        &mut self,
 891        _: &HelixSubstitute,
 892        window: &mut Window,
 893        cx: &mut Context<Self>,
 894    ) {
 895        self.do_helix_substitute(true, window, cx);
 896    }
 897
 898    fn helix_substitute_no_yank(
 899        &mut self,
 900        _: &HelixSubstituteNoYank,
 901        window: &mut Window,
 902        cx: &mut Context<Self>,
 903    ) {
 904        self.do_helix_substitute(false, window, cx);
 905    }
 906
 907    fn helix_select_next(
 908        &mut self,
 909        _: &HelixSelectNext,
 910        window: &mut Window,
 911        cx: &mut Context<Self>,
 912    ) {
 913        self.do_helix_select(Direction::Next, window, cx);
 914    }
 915
 916    fn helix_select_previous(
 917        &mut self,
 918        _: &HelixSelectPrevious,
 919        window: &mut Window,
 920        cx: &mut Context<Self>,
 921    ) {
 922        self.do_helix_select(Direction::Prev, window, cx);
 923    }
 924
 925    fn do_helix_select(
 926        &mut self,
 927        direction: searchable::Direction,
 928        window: &mut Window,
 929        cx: &mut Context<Self>,
 930    ) {
 931        let Some(pane) = self.pane(window, cx) else {
 932            return;
 933        };
 934        let count = Vim::take_count(cx).unwrap_or(1);
 935        Vim::take_forced_motion(cx);
 936        let prior_selections = self.editor_selections(window, cx);
 937
 938        let success = pane.update(cx, |pane, cx| {
 939            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 940                return false;
 941            };
 942            search_bar.update(cx, |search_bar, cx| {
 943                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 944                    return false;
 945                }
 946                search_bar.select_match(direction, count, window, cx);
 947                true
 948            })
 949        });
 950
 951        if !success {
 952            return;
 953        }
 954        if self.mode == Mode::HelixSelect {
 955            self.update_editor(cx, |_vim, editor, cx| {
 956                let snapshot = editor.snapshot(window, cx);
 957                editor.change_selections(SelectionEffects::default(), window, cx, |s| {
 958                    let buffer = snapshot.buffer_snapshot();
 959
 960                    s.select_ranges(
 961                        prior_selections
 962                            .iter()
 963                            .cloned()
 964                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
 965                            .map(|range| {
 966                                let start = range.start.to_offset(buffer);
 967                                let end = range.end.to_offset(buffer);
 968                                start..end
 969                            }),
 970                    );
 971                })
 972            });
 973        }
 974    }
 975}
 976
 977#[cfg(test)]
 978mod test {
 979    use gpui::{KeyBinding, UpdateGlobal, VisualTestContext};
 980    use indoc::indoc;
 981    use project::FakeFs;
 982    use search::{ProjectSearchView, project_search};
 983    use serde_json::json;
 984    use settings::SettingsStore;
 985    use util::path;
 986    use workspace::{DeploySearch, MultiWorkspace};
 987
 988    use crate::{VimAddon, state::Mode, test::VimTestContext};
 989
 990    #[gpui::test]
 991    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
 992        let mut cx = VimTestContext::new(cx, true).await;
 993        cx.enable_helix();
 994        // «
 995        // ˇ
 996        // »
 997        cx.set_state(
 998            indoc! {"
 999            Th«e quiˇ»ck brown
1000            fox jumps over
1001            the lazy dog."},
1002            Mode::HelixNormal,
1003        );
1004
1005        cx.simulate_keystrokes("w");
1006
1007        cx.assert_state(
1008            indoc! {"
1009            The qu«ick ˇ»brown
1010            fox jumps over
1011            the lazy dog."},
1012            Mode::HelixNormal,
1013        );
1014
1015        cx.simulate_keystrokes("w");
1016
1017        cx.assert_state(
1018            indoc! {"
1019            The quick «brownˇ»
1020            fox jumps over
1021            the lazy dog."},
1022            Mode::HelixNormal,
1023        );
1024
1025        cx.simulate_keystrokes("2 b");
1026
1027        cx.assert_state(
1028            indoc! {"
1029            The «ˇquick »brown
1030            fox jumps over
1031            the lazy dog."},
1032            Mode::HelixNormal,
1033        );
1034
1035        cx.simulate_keystrokes("down e up");
1036
1037        cx.assert_state(
1038            indoc! {"
1039            The quicˇk brown
1040            fox jumps over
1041            the lazy dog."},
1042            Mode::HelixNormal,
1043        );
1044
1045        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
1046
1047        cx.simulate_keystroke("b");
1048
1049        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
1050    }
1051
1052    #[gpui::test]
1053    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
1054        let mut cx = VimTestContext::new(cx, true).await;
1055        cx.enable_helix();
1056
1057        // Setup custom keybindings for subword motions so we can use the bindings
1058        // in `simulate_keystroke`.
1059        cx.update(|_window, cx| {
1060            cx.bind_keys([KeyBinding::new(
1061                "w",
1062                crate::motion::NextSubwordStart {
1063                    ignore_punctuation: false,
1064                },
1065                None,
1066            )]);
1067        });
1068
1069        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1070        cx.simulate_keystroke("w");
1071        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1072        cx.simulate_keystroke("w");
1073        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1074        cx.simulate_keystroke("w");
1075        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1076
1077        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1078        cx.simulate_keystroke("w");
1079        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1080        cx.simulate_keystroke("w");
1081        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1082        cx.simulate_keystroke("w");
1083        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1084
1085        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1086        cx.simulate_keystroke("w");
1087        cx.assert_state("«foo_ˇ»bar_baz", Mode::HelixNormal);
1088        cx.simulate_keystroke("w");
1089        cx.assert_state("foo_«bar_ˇ»baz", Mode::HelixNormal);
1090
1091        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1092        cx.simulate_keystroke("w");
1093        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1094        cx.simulate_keystroke("w");
1095        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1096
1097        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1098        cx.simulate_keystroke("w");
1099        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1100        cx.simulate_keystroke("w");
1101        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1102        cx.simulate_keystroke("w");
1103        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1104
1105        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1106        cx.simulate_keystroke("w");
1107        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1108        cx.simulate_keystroke("w");
1109        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1110        cx.simulate_keystroke("w");
1111        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1112        cx.simulate_keystroke("w");
1113        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1114        cx.simulate_keystroke("w");
1115        cx.assert_state("<?php\n\n$some«Variable ˇ»= 2;", Mode::HelixNormal);
1116        cx.simulate_keystroke("w");
1117        cx.assert_state("<?php\n\n$someVariable «= ˇ»2;", Mode::HelixNormal);
1118        cx.simulate_keystroke("w");
1119        cx.assert_state("<?php\n\n$someVariable = «2ˇ»;", Mode::HelixNormal);
1120        cx.simulate_keystroke("w");
1121        cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1122    }
1123
1124    #[gpui::test]
1125    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
1126        let mut cx = VimTestContext::new(cx, true).await;
1127        cx.enable_helix();
1128
1129        // Setup custom keybindings for subword motions so we can use the bindings
1130        // in `simulate_keystroke`.
1131        cx.update(|_window, cx| {
1132            cx.bind_keys([KeyBinding::new(
1133                "e",
1134                crate::motion::NextSubwordEnd {
1135                    ignore_punctuation: false,
1136                },
1137                None,
1138            )]);
1139        });
1140
1141        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1142        cx.simulate_keystroke("e");
1143        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1144        cx.simulate_keystroke("e");
1145        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1146        cx.simulate_keystroke("e");
1147        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1148
1149        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1150        cx.simulate_keystroke("e");
1151        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1152        cx.simulate_keystroke("e");
1153        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1154        cx.simulate_keystroke("e");
1155        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1156
1157        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1158        cx.simulate_keystroke("e");
1159        cx.assert_state("«fooˇ»_bar_baz", Mode::HelixNormal);
1160        cx.simulate_keystroke("e");
1161        cx.assert_state("foo«_barˇ»_baz", Mode::HelixNormal);
1162        cx.simulate_keystroke("e");
1163        cx.assert_state("foo_bar«_bazˇ»", Mode::HelixNormal);
1164
1165        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1166        cx.simulate_keystroke("e");
1167        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1168        cx.simulate_keystroke("e");
1169        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1170        cx.simulate_keystroke("e");
1171        cx.assert_state("fooBar«Bazˇ»", Mode::HelixNormal);
1172
1173        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1174        cx.simulate_keystroke("e");
1175        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1176        cx.simulate_keystroke("e");
1177        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1178        cx.simulate_keystroke("e");
1179        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1180
1181        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1182        cx.simulate_keystroke("e");
1183        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1184        cx.simulate_keystroke("e");
1185        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1186        cx.simulate_keystroke("e");
1187        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1188        cx.simulate_keystroke("e");
1189        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1190        cx.simulate_keystroke("e");
1191        cx.assert_state("<?php\n\n$some«Variableˇ» = 2;", Mode::HelixNormal);
1192        cx.simulate_keystroke("e");
1193        cx.assert_state("<?php\n\n$someVariable« =ˇ» 2;", Mode::HelixNormal);
1194        cx.simulate_keystroke("e");
1195        cx.assert_state("<?php\n\n$someVariable =« 2ˇ»;", Mode::HelixNormal);
1196        cx.simulate_keystroke("e");
1197        cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1198    }
1199
1200    #[gpui::test]
1201    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
1202        let mut cx = VimTestContext::new(cx, true).await;
1203        cx.enable_helix();
1204
1205        // Setup custom keybindings for subword motions so we can use the bindings
1206        // in `simulate_keystroke`.
1207        cx.update(|_window, cx| {
1208            cx.bind_keys([KeyBinding::new(
1209                "b",
1210                crate::motion::PreviousSubwordStart {
1211                    ignore_punctuation: false,
1212                },
1213                None,
1214            )]);
1215        });
1216
1217        cx.set_state("foo.barˇ", Mode::HelixNormal);
1218        cx.simulate_keystroke("b");
1219        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1220        cx.simulate_keystroke("b");
1221        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1222        cx.simulate_keystroke("b");
1223        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1224
1225        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1226        cx.simulate_keystroke("b");
1227        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1228        cx.simulate_keystroke("b");
1229        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1230        cx.simulate_keystroke("b");
1231        cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1232        cx.simulate_keystroke("b");
1233        cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1234
1235        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1236        cx.simulate_keystroke("b");
1237        cx.assert_state("foo_bar_«ˇbaz»", Mode::HelixNormal);
1238        cx.simulate_keystroke("b");
1239        cx.assert_state("foo_«ˇbar_»baz", Mode::HelixNormal);
1240        cx.simulate_keystroke("b");
1241        cx.assert_state("«ˇfoo_»bar_baz", Mode::HelixNormal);
1242
1243        cx.set_state("foo;barˇ", Mode::HelixNormal);
1244        cx.simulate_keystroke("b");
1245        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1246        cx.simulate_keystroke("b");
1247        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1248        cx.simulate_keystroke("b");
1249        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1250
1251        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1252        cx.simulate_keystroke("b");
1253        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1254        cx.simulate_keystroke("b");
1255        cx.assert_state("<?php\n\n$someVariable = «ˇ2»;", Mode::HelixNormal);
1256        cx.simulate_keystroke("b");
1257        cx.assert_state("<?php\n\n$someVariable «ˇ= »2;", Mode::HelixNormal);
1258        cx.simulate_keystroke("b");
1259        cx.assert_state("<?php\n\n$some«ˇVariable »= 2;", Mode::HelixNormal);
1260        cx.simulate_keystroke("b");
1261        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1262        cx.simulate_keystroke("b");
1263        cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1264        cx.simulate_keystroke("b");
1265        cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1266        cx.simulate_keystroke("b");
1267        cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1268
1269        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1270        cx.simulate_keystroke("b");
1271        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1272        cx.simulate_keystroke("b");
1273        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1274        cx.simulate_keystroke("b");
1275        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1276    }
1277
1278    #[gpui::test]
1279    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
1280        let mut cx = VimTestContext::new(cx, true).await;
1281        cx.enable_helix();
1282
1283        // Setup custom keybindings for subword motions so we can use the bindings
1284        // in `simulate_keystrokes`.
1285        cx.update(|_window, cx| {
1286            cx.bind_keys([KeyBinding::new(
1287                "g e",
1288                crate::motion::PreviousSubwordEnd {
1289                    ignore_punctuation: false,
1290                },
1291                None,
1292            )]);
1293        });
1294
1295        cx.set_state("foo.barˇ", Mode::HelixNormal);
1296        cx.simulate_keystrokes("g e");
1297        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1298        cx.simulate_keystrokes("g e");
1299        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1300        cx.simulate_keystrokes("g e");
1301        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1302
1303        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1304        cx.simulate_keystrokes("g e");
1305        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1306        cx.simulate_keystrokes("g e");
1307        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1308        cx.simulate_keystrokes("g e");
1309        cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1310        cx.simulate_keystrokes("g e");
1311        cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1312
1313        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1314        cx.simulate_keystrokes("g e");
1315        cx.assert_state("foo_bar«ˇ_baz»", Mode::HelixNormal);
1316        cx.simulate_keystrokes("g e");
1317        cx.assert_state("foo«ˇ_bar»_baz", Mode::HelixNormal);
1318        cx.simulate_keystrokes("g e");
1319        cx.assert_state("«ˇfoo»_bar_baz", Mode::HelixNormal);
1320
1321        cx.set_state("foo;barˇ", Mode::HelixNormal);
1322        cx.simulate_keystrokes("g e");
1323        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1324        cx.simulate_keystrokes("g e");
1325        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1326        cx.simulate_keystrokes("g e");
1327        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1328
1329        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1330        cx.simulate_keystrokes("g e");
1331        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1332        cx.simulate_keystrokes("g e");
1333        cx.assert_state("<?php\n\n$someVariable =«ˇ 2»;", Mode::HelixNormal);
1334        cx.simulate_keystrokes("g e");
1335        cx.assert_state("<?php\n\n$someVariable«ˇ =» 2;", Mode::HelixNormal);
1336        cx.simulate_keystrokes("g e");
1337        cx.assert_state("<?php\n\n$some«ˇVariable» = 2;", Mode::HelixNormal);
1338        cx.simulate_keystrokes("g e");
1339        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1340        cx.simulate_keystrokes("g e");
1341        cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1342        cx.simulate_keystrokes("g e");
1343        cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1344        cx.simulate_keystrokes("g e");
1345        cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1346
1347        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1348        cx.simulate_keystrokes("g e");
1349        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1350        cx.simulate_keystrokes("g e");
1351        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1352        cx.simulate_keystrokes("g e");
1353        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1354    }
1355
1356    #[gpui::test]
1357    async fn test_delete(cx: &mut gpui::TestAppContext) {
1358        let mut cx = VimTestContext::new(cx, true).await;
1359        cx.enable_helix();
1360
1361        // test delete a selection
1362        cx.set_state(
1363            indoc! {"
1364            The qu«ick ˇ»brown
1365            fox jumps over
1366            the lazy dog."},
1367            Mode::HelixNormal,
1368        );
1369
1370        cx.simulate_keystrokes("d");
1371
1372        cx.assert_state(
1373            indoc! {"
1374            The quˇbrown
1375            fox jumps over
1376            the lazy dog."},
1377            Mode::HelixNormal,
1378        );
1379
1380        // test deleting a single character
1381        cx.simulate_keystrokes("d");
1382
1383        cx.assert_state(
1384            indoc! {"
1385            The quˇrown
1386            fox jumps over
1387            the lazy dog."},
1388            Mode::HelixNormal,
1389        );
1390    }
1391
1392    #[gpui::test]
1393    async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
1394        let mut cx = VimTestContext::new(cx, true).await;
1395
1396        cx.set_state(
1397            indoc! {"
1398            The quick brownˇ
1399            fox jumps over
1400            the lazy dog."},
1401            Mode::HelixNormal,
1402        );
1403
1404        cx.simulate_keystrokes("d");
1405
1406        cx.assert_state(
1407            indoc! {"
1408            The quick brownˇfox jumps over
1409            the lazy dog."},
1410            Mode::HelixNormal,
1411        );
1412    }
1413
1414    // #[gpui::test]
1415    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1416    //     let mut cx = VimTestContext::new(cx, true).await;
1417
1418    //     cx.set_state(
1419    //         indoc! {"
1420    //         The quick brown
1421    //         fox jumps over
1422    //         the lazy dog.ˇ"},
1423    //         Mode::HelixNormal,
1424    //     );
1425
1426    //     cx.simulate_keystrokes("d");
1427
1428    //     cx.assert_state(
1429    //         indoc! {"
1430    //         The quick brown
1431    //         fox jumps over
1432    //         the lazy dog.ˇ"},
1433    //         Mode::HelixNormal,
1434    //     );
1435    // }
1436
1437    #[gpui::test]
1438    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1439        let mut cx = VimTestContext::new(cx, true).await;
1440        cx.enable_helix();
1441
1442        cx.set_state(
1443            indoc! {"
1444            The quˇick brown
1445            fox jumps over
1446            the lazy dog."},
1447            Mode::HelixNormal,
1448        );
1449
1450        cx.simulate_keystrokes("f z");
1451
1452        cx.assert_state(
1453            indoc! {"
1454                The qu«ick brown
1455                fox jumps over
1456                the lazˇ»y dog."},
1457            Mode::HelixNormal,
1458        );
1459
1460        cx.simulate_keystrokes("F e F e");
1461
1462        cx.assert_state(
1463            indoc! {"
1464                The quick brown
1465                fox jumps ov«ˇer
1466                the» lazy dog."},
1467            Mode::HelixNormal,
1468        );
1469
1470        cx.simulate_keystrokes("e 2 F e");
1471
1472        cx.assert_state(
1473            indoc! {"
1474                Th«ˇe quick brown
1475                fox jumps over»
1476                the lazy dog."},
1477            Mode::HelixNormal,
1478        );
1479
1480        cx.simulate_keystrokes("t r t r");
1481
1482        cx.assert_state(
1483            indoc! {"
1484                The quick «brown
1485                fox jumps oveˇ»r
1486                the lazy dog."},
1487            Mode::HelixNormal,
1488        );
1489    }
1490
1491    #[gpui::test]
1492    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1493        let mut cx = VimTestContext::new(cx, true).await;
1494        cx.enable_helix();
1495
1496        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1497
1498        cx.simulate_keystroke("w");
1499
1500        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1501
1502        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1503
1504        cx.simulate_keystroke("b");
1505
1506        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1507    }
1508
1509    #[gpui::test]
1510    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1511        let mut cx = VimTestContext::new(cx, true).await;
1512        cx.enable_helix();
1513        cx.set_state(
1514            indoc! {"
1515            «The ˇ»quick brown
1516            fox jumps over
1517            the lazy dog."},
1518            Mode::HelixNormal,
1519        );
1520
1521        cx.simulate_keystrokes("i");
1522
1523        cx.assert_state(
1524            indoc! {"
1525            ˇThe quick brown
1526            fox jumps over
1527            the lazy dog."},
1528            Mode::Insert,
1529        );
1530    }
1531
1532    #[gpui::test]
1533    async fn test_append(cx: &mut gpui::TestAppContext) {
1534        let mut cx = VimTestContext::new(cx, true).await;
1535        cx.enable_helix();
1536        // test from the end of the selection
1537        cx.set_state(
1538            indoc! {"
1539            «Theˇ» quick brown
1540            fox jumps over
1541            the lazy dog."},
1542            Mode::HelixNormal,
1543        );
1544
1545        cx.simulate_keystrokes("a");
1546
1547        cx.assert_state(
1548            indoc! {"
1549            Theˇ quick brown
1550            fox jumps over
1551            the lazy dog."},
1552            Mode::Insert,
1553        );
1554
1555        // test from the beginning of the selection
1556        cx.set_state(
1557            indoc! {"
1558            «ˇThe» quick brown
1559            fox jumps over
1560            the lazy dog."},
1561            Mode::HelixNormal,
1562        );
1563
1564        cx.simulate_keystrokes("a");
1565
1566        cx.assert_state(
1567            indoc! {"
1568            Theˇ quick brown
1569            fox jumps over
1570            the lazy dog."},
1571            Mode::Insert,
1572        );
1573    }
1574
1575    #[gpui::test]
1576    async fn test_replace(cx: &mut gpui::TestAppContext) {
1577        let mut cx = VimTestContext::new(cx, true).await;
1578        cx.enable_helix();
1579
1580        // No selection (single character)
1581        cx.set_state("ˇaa", Mode::HelixNormal);
1582
1583        cx.simulate_keystrokes("r x");
1584
1585        cx.assert_state("ˇxa", Mode::HelixNormal);
1586
1587        // Cursor at the beginning
1588        cx.set_state("«ˇaa»", Mode::HelixNormal);
1589
1590        cx.simulate_keystrokes("r x");
1591
1592        cx.assert_state("«ˇxx»", Mode::HelixNormal);
1593
1594        // Cursor at the end
1595        cx.set_state("«aaˇ»", Mode::HelixNormal);
1596
1597        cx.simulate_keystrokes("r x");
1598
1599        cx.assert_state("«xxˇ»", Mode::HelixNormal);
1600    }
1601
1602    #[gpui::test]
1603    async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1604        let mut cx = VimTestContext::new(cx, true).await;
1605        cx.enable_helix();
1606
1607        // Test yanking current character with no selection
1608        cx.set_state("hello ˇworld", Mode::HelixNormal);
1609        cx.simulate_keystrokes("y");
1610
1611        // Test cursor remains at the same position after yanking single character
1612        cx.assert_state("hello ˇworld", Mode::HelixNormal);
1613        cx.shared_clipboard().assert_eq("w");
1614
1615        // Move cursor and yank another character
1616        cx.simulate_keystrokes("l");
1617        cx.simulate_keystrokes("y");
1618        cx.shared_clipboard().assert_eq("o");
1619
1620        // Test yanking with existing selection
1621        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1622        cx.simulate_keystrokes("y");
1623        cx.shared_clipboard().assert_eq("worl");
1624        cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1625
1626        // Test yanking in select mode character by character
1627        cx.set_state("hello ˇworld", Mode::HelixNormal);
1628        cx.simulate_keystroke("v");
1629        cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1630        cx.simulate_keystroke("y");
1631        cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1632        cx.shared_clipboard().assert_eq("w");
1633    }
1634
1635    #[gpui::test]
1636    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1637        let mut cx = VimTestContext::new(cx, true).await;
1638        cx.enable_helix();
1639
1640        // First copy some text to clipboard
1641        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1642        cx.simulate_keystrokes("y");
1643
1644        // Test paste with shift-r on single cursor
1645        cx.set_state("foo ˇbar", Mode::HelixNormal);
1646        cx.simulate_keystrokes("shift-r");
1647
1648        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1649
1650        // Test paste with shift-r on selection
1651        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1652        cx.simulate_keystrokes("shift-r");
1653
1654        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1655    }
1656
1657    #[gpui::test]
1658    async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1659        let mut cx = VimTestContext::new(cx, true).await;
1660
1661        assert_eq!(cx.mode(), Mode::Normal);
1662        cx.enable_helix();
1663
1664        cx.simulate_keystrokes("v");
1665        assert_eq!(cx.mode(), Mode::HelixSelect);
1666        cx.simulate_keystrokes("escape");
1667        assert_eq!(cx.mode(), Mode::HelixNormal);
1668    }
1669
1670    #[gpui::test]
1671    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1672        let mut cx = VimTestContext::new(cx, true).await;
1673        cx.enable_helix();
1674
1675        // Make a modification at a specific location
1676        cx.set_state("ˇhello", Mode::HelixNormal);
1677        assert_eq!(cx.mode(), Mode::HelixNormal);
1678        cx.simulate_keystrokes("i");
1679        assert_eq!(cx.mode(), Mode::Insert);
1680        cx.simulate_keystrokes("escape");
1681        assert_eq!(cx.mode(), Mode::HelixNormal);
1682    }
1683
1684    #[gpui::test]
1685    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1686        let mut cx = VimTestContext::new(cx, true).await;
1687        cx.enable_helix();
1688
1689        // Make a modification at a specific location
1690        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1691        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1692        cx.simulate_keystrokes("i");
1693        cx.simulate_keystrokes("escape");
1694        cx.simulate_keystrokes("i");
1695        cx.simulate_keystrokes("m o d i f i e d space");
1696        cx.simulate_keystrokes("escape");
1697
1698        // TODO: this fails, because state is no longer helix
1699        cx.assert_state(
1700            "line one\nline modified ˇtwo\nline three",
1701            Mode::HelixNormal,
1702        );
1703
1704        // Move cursor away from the modification
1705        cx.simulate_keystrokes("up");
1706
1707        // Use "g ." to go back to last modification
1708        cx.simulate_keystrokes("g .");
1709
1710        // Verify we're back at the modification location and still in HelixNormal mode
1711        cx.assert_state(
1712            "line one\nline modifiedˇ two\nline three",
1713            Mode::HelixNormal,
1714        );
1715    }
1716
1717    #[gpui::test]
1718    async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1719        let mut cx = VimTestContext::new(cx, true).await;
1720        cx.set_state(
1721            "line one\nline ˇtwo\nline three\nline four",
1722            Mode::HelixNormal,
1723        );
1724        cx.simulate_keystrokes("2 x");
1725        cx.assert_state(
1726            "line one\n«line two\nline three\nˇ»line four",
1727            Mode::HelixNormal,
1728        );
1729
1730        // Test extending existing line selection
1731        cx.set_state(
1732            indoc! {"
1733            li«ˇne one
1734            li»ne two
1735            line three
1736            line four"},
1737            Mode::HelixNormal,
1738        );
1739        cx.simulate_keystrokes("x");
1740        cx.assert_state(
1741            indoc! {"
1742            «line one
1743            line two
1744            ˇ»line three
1745            line four"},
1746            Mode::HelixNormal,
1747        );
1748
1749        // Pressing x in empty line, select next line (because helix considers cursor a selection)
1750        cx.set_state(
1751            indoc! {"
1752            line one
1753            ˇ
1754            line three
1755            line four
1756            line five
1757            line six"},
1758            Mode::HelixNormal,
1759        );
1760        cx.simulate_keystrokes("x");
1761        cx.assert_state(
1762            indoc! {"
1763            line one
1764            «
1765            line three
1766            ˇ»line four
1767            line five
1768            line six"},
1769            Mode::HelixNormal,
1770        );
1771
1772        // Another x should only select the next line
1773        cx.simulate_keystrokes("x");
1774        cx.assert_state(
1775            indoc! {"
1776            line one
1777            «
1778            line three
1779            line four
1780            ˇ»line five
1781            line six"},
1782            Mode::HelixNormal,
1783        );
1784
1785        // Empty line with count selects extra + count lines
1786        cx.set_state(
1787            indoc! {"
1788            line one
1789            ˇ
1790            line three
1791            line four
1792            line five"},
1793            Mode::HelixNormal,
1794        );
1795        cx.simulate_keystrokes("2 x");
1796        cx.assert_state(
1797            indoc! {"
1798            line one
1799            «
1800            line three
1801            line four
1802            ˇ»line five"},
1803            Mode::HelixNormal,
1804        );
1805
1806        // Compare empty vs non-empty line behavior
1807        cx.set_state(
1808            indoc! {"
1809            ˇnon-empty line
1810            line two
1811            line three"},
1812            Mode::HelixNormal,
1813        );
1814        cx.simulate_keystrokes("x");
1815        cx.assert_state(
1816            indoc! {"
1817            «non-empty line
1818            ˇ»line two
1819            line three"},
1820            Mode::HelixNormal,
1821        );
1822
1823        // Same test but with empty line - should select one extra
1824        cx.set_state(
1825            indoc! {"
1826            ˇ
1827            line two
1828            line three"},
1829            Mode::HelixNormal,
1830        );
1831        cx.simulate_keystrokes("x");
1832        cx.assert_state(
1833            indoc! {"
1834            «
1835            line two
1836            ˇ»line three"},
1837            Mode::HelixNormal,
1838        );
1839
1840        // Test selecting multiple lines with count
1841        cx.set_state(
1842            indoc! {"
1843            ˇline one
1844            line two
1845            line threeˇ
1846            line four
1847            line five"},
1848            Mode::HelixNormal,
1849        );
1850        cx.simulate_keystrokes("x");
1851        cx.assert_state(
1852            indoc! {"
1853            «line one
1854            ˇ»line two
1855            «line three
1856            ˇ»line four
1857            line five"},
1858            Mode::HelixNormal,
1859        );
1860        cx.simulate_keystrokes("x");
1861        // Adjacent line selections stay separate (not merged)
1862        cx.assert_state(
1863            indoc! {"
1864            «line one
1865            line two
1866            ˇ»«line three
1867            line four
1868            ˇ»line five"},
1869            Mode::HelixNormal,
1870        );
1871
1872        // Test selecting with an empty line below the current line
1873        cx.set_state(
1874            indoc! {"
1875            line one
1876            line twoˇ
1877
1878            line four
1879            line five"},
1880            Mode::HelixNormal,
1881        );
1882        cx.simulate_keystrokes("x");
1883        cx.assert_state(
1884            indoc! {"
1885            line one
1886            «line two
1887            ˇ»
1888            line four
1889            line five"},
1890            Mode::HelixNormal,
1891        );
1892        cx.simulate_keystrokes("x");
1893        cx.assert_state(
1894            indoc! {"
1895            line one
1896            «line two
1897
1898            ˇ»line four
1899            line five"},
1900            Mode::HelixNormal,
1901        );
1902        cx.simulate_keystrokes("x");
1903        cx.assert_state(
1904            indoc! {"
1905            line one
1906            «line two
1907
1908            line four
1909            ˇ»line five"},
1910            Mode::HelixNormal,
1911        );
1912    }
1913
1914    #[gpui::test]
1915    async fn test_helix_insert_before_after_select_lines(cx: &mut gpui::TestAppContext) {
1916        let mut cx = VimTestContext::new(cx, true).await;
1917
1918        cx.set_state(
1919            "line one\nline ˇtwo\nline three\nline four",
1920            Mode::HelixNormal,
1921        );
1922        cx.simulate_keystrokes("2 x");
1923        cx.assert_state(
1924            "line one\n«line two\nline three\nˇ»line four",
1925            Mode::HelixNormal,
1926        );
1927        cx.simulate_keystrokes("o");
1928        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1929
1930        cx.set_state(
1931            "line one\nline ˇtwo\nline three\nline four",
1932            Mode::HelixNormal,
1933        );
1934        cx.simulate_keystrokes("2 x");
1935        cx.assert_state(
1936            "line one\n«line two\nline three\nˇ»line four",
1937            Mode::HelixNormal,
1938        );
1939        cx.simulate_keystrokes("shift-o");
1940        cx.assert_state("line one\nˇ\nline two\nline three\nline four", Mode::Insert);
1941    }
1942
1943    #[gpui::test]
1944    async fn test_helix_insert_before_after_helix_select(cx: &mut gpui::TestAppContext) {
1945        let mut cx = VimTestContext::new(cx, true).await;
1946        cx.enable_helix();
1947
1948        // Test new line in selection direction
1949        cx.set_state(
1950            "ˇline one\nline two\nline three\nline four",
1951            Mode::HelixNormal,
1952        );
1953        cx.simulate_keystrokes("v j j");
1954        cx.assert_state(
1955            "«line one\nline two\nlˇ»ine three\nline four",
1956            Mode::HelixSelect,
1957        );
1958        cx.simulate_keystrokes("o");
1959        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1960
1961        cx.set_state(
1962            "line one\nline two\nˇline three\nline four",
1963            Mode::HelixNormal,
1964        );
1965        cx.simulate_keystrokes("v k k");
1966        cx.assert_state(
1967            "«ˇline one\nline two\nl»ine three\nline four",
1968            Mode::HelixSelect,
1969        );
1970        cx.simulate_keystrokes("shift-o");
1971        cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1972
1973        // Test new line in opposite selection direction
1974        cx.set_state(
1975            "ˇline one\nline two\nline three\nline four",
1976            Mode::HelixNormal,
1977        );
1978        cx.simulate_keystrokes("v j j");
1979        cx.assert_state(
1980            "«line one\nline two\nlˇ»ine three\nline four",
1981            Mode::HelixSelect,
1982        );
1983        cx.simulate_keystrokes("shift-o");
1984        cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1985
1986        cx.set_state(
1987            "line one\nline two\nˇline three\nline four",
1988            Mode::HelixNormal,
1989        );
1990        cx.simulate_keystrokes("v k k");
1991        cx.assert_state(
1992            "«ˇline one\nline two\nl»ine three\nline four",
1993            Mode::HelixSelect,
1994        );
1995        cx.simulate_keystrokes("o");
1996        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1997    }
1998
1999    #[gpui::test]
2000    async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
2001        let mut cx = VimTestContext::new(cx, true).await;
2002
2003        assert_eq!(cx.mode(), Mode::Normal);
2004        cx.enable_helix();
2005
2006        cx.set_state("ˇhello", Mode::HelixNormal);
2007        cx.simulate_keystrokes("l v l l");
2008        cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
2009    }
2010
2011    #[gpui::test]
2012    async fn test_helix_select_end_of_line(cx: &mut gpui::TestAppContext) {
2013        let mut cx = VimTestContext::new(cx, true).await;
2014        cx.enable_helix();
2015
2016        // v g l d should delete to end of line without consuming the newline
2017        cx.set_state("ˇThe quick brown\nfox jumps over", Mode::HelixNormal);
2018        cx.simulate_keystrokes("v g l d");
2019        cx.assert_state("ˇ\nfox jumps over", Mode::HelixNormal);
2020
2021        // same from the middle of a line — cursor lands on the last
2022        // remaining character (the space) after delete
2023        cx.set_state("The ˇquick brown\nfox jumps over", Mode::HelixNormal);
2024        cx.simulate_keystrokes("v g l d");
2025        cx.assert_state("Theˇ \nfox jumps over", Mode::HelixNormal);
2026    }
2027
2028    #[gpui::test]
2029    async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
2030        let mut cx = VimTestContext::new(cx, true).await;
2031
2032        assert_eq!(cx.mode(), Mode::Normal);
2033        cx.enable_helix();
2034
2035        // Start with multiple cursors (no selections)
2036        cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
2037
2038        // Enter select mode and move right twice
2039        cx.simulate_keystrokes("v l l");
2040
2041        // Each cursor should independently create and extend its own selection
2042        cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
2043    }
2044
2045    #[gpui::test]
2046    async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
2047        let mut cx = VimTestContext::new(cx, true).await;
2048
2049        cx.set_state("ˇone two", Mode::Normal);
2050        cx.simulate_keystrokes("v w");
2051        cx.assert_state("«one tˇ»wo", Mode::Visual);
2052
2053        // In Vim, this selects "t". In helix selections stops just before "t"
2054
2055        cx.enable_helix();
2056        cx.set_state("ˇone two", Mode::HelixNormal);
2057        cx.simulate_keystrokes("v w");
2058        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2059    }
2060
2061    #[gpui::test]
2062    async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
2063        let mut cx = VimTestContext::new(cx, true).await;
2064
2065        cx.set_state("ˇone two", Mode::Normal);
2066        cx.simulate_keystrokes("v w");
2067        cx.assert_state("«one tˇ»wo", Mode::Visual);
2068        cx.simulate_keystrokes("escape");
2069        cx.assert_state("one ˇtwo", Mode::Normal);
2070
2071        cx.enable_helix();
2072        cx.set_state("ˇone two", Mode::HelixNormal);
2073        cx.simulate_keystrokes("v w");
2074        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2075        cx.simulate_keystrokes("escape");
2076        cx.assert_state("«one ˇ»two", Mode::HelixNormal);
2077    }
2078
2079    #[gpui::test]
2080    async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
2081        let mut cx = VimTestContext::new(cx, true).await;
2082        cx.enable_helix();
2083
2084        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2085        cx.simulate_keystrokes("w");
2086        cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
2087
2088        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2089        cx.simulate_keystrokes("e");
2090        cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
2091    }
2092
2093    #[gpui::test]
2094    async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
2095        let mut cx = VimTestContext::new(cx, true).await;
2096        cx.enable_helix();
2097
2098        cx.set_state("ˇone two three", Mode::HelixNormal);
2099        cx.simulate_keystrokes("l l v h h h");
2100        cx.assert_state("«ˇone» two three", Mode::HelixSelect);
2101    }
2102
2103    #[gpui::test]
2104    async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
2105        let mut cx = VimTestContext::new(cx, true).await;
2106        cx.enable_helix();
2107
2108        cx.set_state("ˇone two one", Mode::HelixNormal);
2109        cx.simulate_keystrokes("x");
2110        cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
2111        cx.simulate_keystrokes("s o n e");
2112        cx.run_until_parked();
2113        cx.simulate_keystrokes("enter");
2114        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2115
2116        cx.simulate_keystrokes("x");
2117        cx.simulate_keystrokes("s");
2118        cx.run_until_parked();
2119        cx.simulate_keystrokes("enter");
2120        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2121
2122        // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
2123        // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
2124        // cx.simulate_keystrokes("s o n e enter");
2125        // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
2126    }
2127
2128    #[gpui::test]
2129    async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
2130        let mut cx = VimTestContext::new(cx, true).await;
2131
2132        cx.set_state("ˇhello two one two one two one", Mode::Visual);
2133        cx.simulate_keystrokes("/ o n e");
2134        cx.simulate_keystrokes("enter");
2135        cx.simulate_keystrokes("n n");
2136        cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
2137
2138        cx.set_state("ˇhello two one two one two one", Mode::Normal);
2139        cx.simulate_keystrokes("/ o n e");
2140        cx.simulate_keystrokes("enter");
2141        cx.simulate_keystrokes("n n");
2142        cx.assert_state("hello two one two one two ˇone", Mode::Normal);
2143
2144        cx.set_state("ˇhello two one two one two one", Mode::Normal);
2145        cx.simulate_keystrokes("/ o n e");
2146        cx.simulate_keystrokes("enter");
2147        cx.simulate_keystrokes("n g n g n");
2148        cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
2149
2150        cx.enable_helix();
2151
2152        cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
2153        cx.simulate_keystrokes("/ o n e");
2154        cx.simulate_keystrokes("enter");
2155        cx.simulate_keystrokes("n n");
2156        cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
2157
2158        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2159        cx.simulate_keystrokes("/ o n e");
2160        cx.simulate_keystrokes("enter");
2161        cx.simulate_keystrokes("n n");
2162        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2163    }
2164
2165    #[gpui::test]
2166    async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
2167        let mut cx = VimTestContext::new(cx, true).await;
2168        cx.enable_helix();
2169
2170        // Three occurrences of "one". After selecting all three with `n n`,
2171        // pressing `n` again wraps the search to the first occurrence.
2172        // The prior selections (at higher offsets) are chained before the
2173        // wrapped selection (at a lower offset), producing unsorted anchors
2174        // that cause `rope::Cursor::summary` to panic with
2175        // "cannot summarize backward".
2176        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2177        cx.simulate_keystrokes("/ o n e");
2178        cx.simulate_keystrokes("enter");
2179        cx.simulate_keystrokes("n n n");
2180        // Should not panic; all three occurrences should remain selected.
2181        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2182    }
2183
2184    #[gpui::test]
2185    async fn test_helix_select_next_match_wrapping_from_normal(cx: &mut gpui::TestAppContext) {
2186        let mut cx = VimTestContext::new(cx, true).await;
2187        cx.enable_helix();
2188
2189        // Exact repro for #51573: start in HelixNormal, search, then `v` to
2190        // enter HelixSelect, then `n` past last match.
2191        //
2192        // In HelixNormal, search collapses the cursor to the match start.
2193        // Pressing `v` expands by only one character, creating a partial
2194        // selection that overlaps the full match range when the search wraps.
2195        // The overlapping ranges must be merged (not just deduped) to avoid
2196        // a backward-seeking rope cursor panic.
2197        cx.set_state(
2198            indoc! {"
2199                searˇch term
2200                stuff
2201                search term
2202                other stuff
2203            "},
2204            Mode::HelixNormal,
2205        );
2206        cx.simulate_keystrokes("/ t e r m");
2207        cx.simulate_keystrokes("enter");
2208        cx.simulate_keystrokes("v");
2209        cx.simulate_keystrokes("n");
2210        cx.simulate_keystrokes("n");
2211        // Should not panic when wrapping past last match.
2212        cx.assert_state(
2213            indoc! {"
2214                search «termˇ»
2215                stuff
2216                search «termˇ»
2217                other stuff
2218            "},
2219            Mode::HelixSelect,
2220        );
2221    }
2222
2223    #[gpui::test]
2224    async fn test_helix_select_star_then_match(cx: &mut gpui::TestAppContext) {
2225        let mut cx = VimTestContext::new(cx, true).await;
2226        cx.enable_helix();
2227
2228        // Repro attempts for #52852: `*` searches for word under cursor,
2229        // `v` enters select, `n` accumulates matches, `m` triggers match mode.
2230        // Try multiple cursor positions and match counts.
2231
2232        // Cursor on first occurrence, 3 more occurrences to select through
2233        cx.set_state(
2234            indoc! {"
2235                ˇone two one three one four one
2236            "},
2237            Mode::HelixNormal,
2238        );
2239        cx.simulate_keystrokes("*");
2240        cx.simulate_keystrokes("v");
2241        cx.simulate_keystrokes("n n n");
2242        // Should not panic on wrapping `n`.
2243
2244        // Cursor in the middle of text before matches
2245        cx.set_state(
2246            indoc! {"
2247                heˇllo one two one three one
2248            "},
2249            Mode::HelixNormal,
2250        );
2251        cx.simulate_keystrokes("*");
2252        cx.simulate_keystrokes("v");
2253        cx.simulate_keystrokes("n");
2254        // Should not panic.
2255
2256        // The original #52852 sequence: * v n n n then m m
2257        cx.set_state(
2258            indoc! {"
2259                fn ˇfoo() { bar(foo()) }
2260                fn baz() { foo() }
2261            "},
2262            Mode::HelixNormal,
2263        );
2264        cx.simulate_keystrokes("*");
2265        cx.simulate_keystrokes("v");
2266        cx.simulate_keystrokes("n n n");
2267        cx.simulate_keystrokes("m m");
2268        // Should not panic.
2269    }
2270
2271    #[gpui::test]
2272    async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
2273        let mut cx = VimTestContext::new(cx, true).await;
2274
2275        cx.set_state("ˇone two", Mode::HelixNormal);
2276        cx.simulate_keystrokes("c");
2277        cx.assert_state("ˇne two", Mode::Insert);
2278
2279        cx.set_state("«oneˇ» two", Mode::HelixNormal);
2280        cx.simulate_keystrokes("c");
2281        cx.assert_state("ˇ two", Mode::Insert);
2282
2283        cx.set_state(
2284            indoc! {"
2285            oneˇ two
2286            three
2287            "},
2288            Mode::HelixNormal,
2289        );
2290        cx.simulate_keystrokes("x c");
2291        cx.assert_state(
2292            indoc! {"
2293            ˇ
2294            three
2295            "},
2296            Mode::Insert,
2297        );
2298
2299        cx.set_state(
2300            indoc! {"
2301            one twoˇ
2302            three
2303            "},
2304            Mode::HelixNormal,
2305        );
2306        cx.simulate_keystrokes("c");
2307        cx.assert_state(
2308            indoc! {"
2309            one twoˇthree
2310            "},
2311            Mode::Insert,
2312        );
2313
2314        // Helix doesn't set the cursor to the first non-blank one when
2315        // replacing lines: it uses language-dependent indent queries instead.
2316        cx.set_state(
2317            indoc! {"
2318            one two
2319            «    indented
2320            three not indentedˇ»
2321            "},
2322            Mode::HelixNormal,
2323        );
2324        cx.simulate_keystrokes("c");
2325        cx.set_state(
2326            indoc! {"
2327            one two
2328            ˇ
2329            "},
2330            Mode::Insert,
2331        );
2332    }
2333
2334    #[gpui::test]
2335    async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
2336        let mut cx = VimTestContext::new(cx, true).await;
2337        cx.enable_helix();
2338
2339        // Test g l moves to last character, not after it
2340        cx.set_state("hello ˇworld!", Mode::HelixNormal);
2341        cx.simulate_keystrokes("g l");
2342        cx.assert_state("hello worldˇ!", Mode::HelixNormal);
2343
2344        // Test with Chinese characters, test if work with UTF-8?
2345        cx.set_state("ˇ你好世界", Mode::HelixNormal);
2346        cx.simulate_keystrokes("g l");
2347        cx.assert_state("你好世ˇ界", Mode::HelixNormal);
2348
2349        // Test with end of line
2350        cx.set_state("endˇ", Mode::HelixNormal);
2351        cx.simulate_keystrokes("g l");
2352        cx.assert_state("enˇd", Mode::HelixNormal);
2353
2354        // Test with empty line
2355        cx.set_state(
2356            indoc! {"
2357                hello
2358                ˇ
2359                world"},
2360            Mode::HelixNormal,
2361        );
2362        cx.simulate_keystrokes("g l");
2363        cx.assert_state(
2364            indoc! {"
2365                hello
2366                ˇ
2367                world"},
2368            Mode::HelixNormal,
2369        );
2370
2371        // Test with multiple lines
2372        cx.set_state(
2373            indoc! {"
2374                ˇfirst line
2375                second line
2376                third line"},
2377            Mode::HelixNormal,
2378        );
2379        cx.simulate_keystrokes("g l");
2380        cx.assert_state(
2381            indoc! {"
2382                first linˇe
2383                second line
2384                third line"},
2385            Mode::HelixNormal,
2386        );
2387    }
2388
2389    #[gpui::test]
2390    async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
2391        VimTestContext::init(cx);
2392
2393        let fs = FakeFs::new(cx.background_executor.clone());
2394        fs.insert_tree(
2395            path!("/dir"),
2396            json!({
2397                "file_a.rs": "// File A.",
2398                "file_b.rs": "// File B.",
2399            }),
2400        )
2401        .await;
2402
2403        let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2404        let window_handle =
2405            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2406        let workspace = window_handle
2407            .read_with(cx, |mw, _| mw.workspace().clone())
2408            .unwrap();
2409
2410        cx.update(|cx| {
2411            VimTestContext::init_keybindings(true, cx);
2412            SettingsStore::update_global(cx, |store, cx| {
2413                store.update_user_settings(cx, |store| store.helix_mode = Some(true));
2414            })
2415        });
2416
2417        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2418
2419        workspace.update_in(cx, |workspace, window, cx| {
2420            ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
2421        });
2422
2423        let search_view = workspace.update_in(cx, |workspace, _, cx| {
2424            workspace
2425                .active_pane()
2426                .read(cx)
2427                .items()
2428                .find_map(|item| item.downcast::<ProjectSearchView>())
2429                .expect("Project search view should be active")
2430        });
2431
2432        project_search::perform_project_search(&search_view, "File A", cx);
2433
2434        search_view.update(cx, |search_view, cx| {
2435            let vim_mode = search_view
2436                .results_editor()
2437                .read(cx)
2438                .addon::<VimAddon>()
2439                .map(|addon| addon.entity.read(cx).mode);
2440
2441            assert_eq!(vim_mode, Some(Mode::HelixNormal));
2442        });
2443    }
2444
2445    #[gpui::test]
2446    async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
2447        let mut cx = VimTestContext::new(cx, true).await;
2448        cx.enable_helix();
2449
2450        // Start with a selection
2451        cx.set_state(
2452            indoc! {"
2453            «lineˇ» one
2454            line two
2455            line three
2456            line four
2457            line five"},
2458            Mode::HelixNormal,
2459        );
2460
2461        // Scroll down, selection should collapse
2462        cx.simulate_keystrokes("ctrl-d");
2463        cx.assert_state(
2464            indoc! {"
2465            line one
2466            line two
2467            line three
2468            line four
2469            line fiveˇ"},
2470            Mode::HelixNormal,
2471        );
2472
2473        // Make a new selection
2474        cx.simulate_keystroke("b");
2475        cx.assert_state(
2476            indoc! {"
2477            line one
2478            line two
2479            line three
2480            line four
2481            line «ˇfive»"},
2482            Mode::HelixNormal,
2483        );
2484
2485        // And scroll up, once again collapsing the selection.
2486        cx.simulate_keystroke("ctrl-u");
2487        cx.assert_state(
2488            indoc! {"
2489            line one
2490            line two
2491            line three
2492            line ˇfour
2493            line five"},
2494            Mode::HelixNormal,
2495        );
2496
2497        // Enter select mode
2498        cx.simulate_keystroke("v");
2499        cx.assert_state(
2500            indoc! {"
2501            line one
2502            line two
2503            line three
2504            line «fˇ»our
2505            line five"},
2506            Mode::HelixSelect,
2507        );
2508
2509        // And now the selection should be kept/expanded.
2510        cx.simulate_keystroke("ctrl-d");
2511        cx.assert_state(
2512            indoc! {"
2513            line one
2514            line two
2515            line three
2516            line «four
2517            line fiveˇ»"},
2518            Mode::HelixSelect,
2519        );
2520    }
2521
2522    #[gpui::test]
2523    async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
2524        let mut cx = VimTestContext::new(cx, true).await;
2525        cx.enable_helix();
2526
2527        // Ensure that, when lines are selected using `x`, pressing `shift-a`
2528        // actually puts the cursor at the end of the selected lines and not at
2529        // the end of the line below.
2530        cx.set_state(
2531            indoc! {"
2532            line oˇne
2533            line two"},
2534            Mode::HelixNormal,
2535        );
2536
2537        cx.simulate_keystrokes("x");
2538        cx.assert_state(
2539            indoc! {"
2540            «line one
2541            ˇ»line two"},
2542            Mode::HelixNormal,
2543        );
2544
2545        cx.simulate_keystrokes("shift-a");
2546        cx.assert_state(
2547            indoc! {"
2548            line oneˇ
2549            line two"},
2550            Mode::Insert,
2551        );
2552
2553        cx.set_state(
2554            indoc! {"
2555            line «one
2556            lineˇ» two"},
2557            Mode::HelixNormal,
2558        );
2559
2560        cx.simulate_keystrokes("shift-a");
2561        cx.assert_state(
2562            indoc! {"
2563            line one
2564            line twoˇ"},
2565            Mode::Insert,
2566        );
2567    }
2568
2569    #[gpui::test]
2570    async fn test_helix_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2571        let mut cx = VimTestContext::new(cx, true).await;
2572        cx.enable_helix();
2573
2574        cx.set_state("«Hällöˇ» Wörld", Mode::HelixNormal);
2575        cx.simulate_keystrokes("r 1");
2576        cx.assert_state("«11111ˇ» Wörld", Mode::HelixNormal);
2577
2578        cx.set_state("«e\u{301}ˇ»", Mode::HelixNormal);
2579        cx.simulate_keystrokes("r 1");
2580        cx.assert_state("«1ˇ»", Mode::HelixNormal);
2581
2582        cx.set_state("«🙂ˇ»", Mode::HelixNormal);
2583        cx.simulate_keystrokes("r 1");
2584        cx.assert_state("«1ˇ»", Mode::HelixNormal);
2585    }
2586}