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