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