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                let mut edits = Vec::new();
 715                let mut selection_info = Vec::new();
 716                for selection in &selections {
 717                    let mut range = selection.range();
 718                    let was_empty = range.is_empty();
 719                    let was_reversed = selection.reversed;
 720
 721                    if was_empty {
 722                        range.end = movement::saturating_right(&display_map, range.start);
 723                    }
 724
 725                    let byte_range = range.start.to_offset(&display_map, Bias::Left)
 726                        ..range.end.to_offset(&display_map, Bias::Left);
 727
 728                    let snapshot = display_map.buffer_snapshot();
 729                    let grapheme_count = snapshot.grapheme_count_for_range(&byte_range);
 730                    let anchor = snapshot.anchor_before(byte_range.start);
 731
 732                    selection_info.push((anchor, grapheme_count, was_empty, was_reversed));
 733
 734                    if !byte_range.is_empty() {
 735                        let replacement_text = text.repeat(grapheme_count);
 736                        edits.push((byte_range, replacement_text));
 737                    }
 738                }
 739
 740                editor.edit(edits, cx);
 741
 742                // Restore selections based on original info
 743                let snapshot = editor.buffer().read(cx).snapshot(cx);
 744                let ranges: Vec<_> = selection_info
 745                    .into_iter()
 746                    .map(|(start_anchor, grapheme_count, was_empty, was_reversed)| {
 747                        let start_point = start_anchor.to_point(&snapshot);
 748                        if was_empty {
 749                            start_point..start_point
 750                        } else {
 751                            let replacement_len = text.len() * grapheme_count;
 752                            let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
 753                            let end_point = snapshot.offset_to_point(end_offset);
 754                            if was_reversed {
 755                                end_point..start_point
 756                            } else {
 757                                start_point..end_point
 758                            }
 759                        }
 760                    })
 761                    .collect();
 762
 763                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 764                    s.select_ranges(ranges);
 765                });
 766            });
 767        });
 768        self.switch_mode(Mode::HelixNormal, true, window, cx);
 769    }
 770
 771    pub fn helix_goto_last_modification(
 772        &mut self,
 773        _: &HelixGotoLastModification,
 774        window: &mut Window,
 775        cx: &mut Context<Self>,
 776    ) {
 777        self.jump(".".into(), false, false, window, cx);
 778    }
 779
 780    pub fn helix_select_lines(
 781        &mut self,
 782        _: &HelixSelectLine,
 783        window: &mut Window,
 784        cx: &mut Context<Self>,
 785    ) {
 786        let count = Vim::take_count(cx).unwrap_or(1);
 787        self.update_editor(cx, |_, editor, cx| {
 788            editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 789            let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
 790            let mut selections = editor.selections.all::<Point>(&display_map);
 791            let max_point = display_map.buffer_snapshot().max_point();
 792            let buffer_snapshot = &display_map.buffer_snapshot();
 793
 794            for selection in &mut selections {
 795                // Start always goes to column 0 of the first selected line
 796                let start_row = selection.start.row;
 797                let current_end_row = selection.end.row;
 798
 799                // Check if cursor is on empty line by checking first character
 800                let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
 801                let first_char = buffer_snapshot.chars_at(line_start_offset).next();
 802                let extra_line = if first_char == Some('\n') && selection.is_empty() {
 803                    1
 804                } else {
 805                    0
 806                };
 807
 808                let end_row = current_end_row + count as u32 + extra_line;
 809
 810                selection.start = Point::new(start_row, 0);
 811                selection.end = if end_row > max_point.row {
 812                    max_point
 813                } else {
 814                    Point::new(end_row, 0)
 815                };
 816                selection.reversed = false;
 817            }
 818
 819            editor.change_selections(Default::default(), window, cx, |s| {
 820                s.select(selections);
 821            });
 822        });
 823    }
 824
 825    fn helix_keep_newest_selection(
 826        &mut self,
 827        _: &HelixKeepNewestSelection,
 828        window: &mut Window,
 829        cx: &mut Context<Self>,
 830    ) {
 831        self.update_editor(cx, |_, editor, cx| {
 832            let newest = editor
 833                .selections
 834                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
 835            editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
 836        });
 837    }
 838
 839    fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
 840        self.update_editor(cx, |vim, editor, cx| {
 841            editor.set_clip_at_line_ends(false, cx);
 842            editor.transact(window, cx, |editor, window, cx| {
 843                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 844                    s.move_with(&mut |map, selection| {
 845                        if selection.start == selection.end {
 846                            selection.end = movement::right(map, selection.end);
 847                        }
 848
 849                        // If the selection starts and ends on a newline, we exclude the last one.
 850                        if !selection.is_empty()
 851                            && selection.start.column() == 0
 852                            && selection.end.column() == 0
 853                        {
 854                            selection.end = movement::left(map, selection.end);
 855                        }
 856                    })
 857                });
 858                if yank {
 859                    vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
 860                }
 861                let selections = editor
 862                    .selections
 863                    .all::<Point>(&editor.display_snapshot(cx))
 864                    .into_iter();
 865                let edits = selections.map(|selection| (selection.start..selection.end, ""));
 866                editor.edit(edits, cx);
 867            });
 868        });
 869        self.switch_mode(Mode::Insert, true, window, cx);
 870    }
 871
 872    fn helix_substitute(
 873        &mut self,
 874        _: &HelixSubstitute,
 875        window: &mut Window,
 876        cx: &mut Context<Self>,
 877    ) {
 878        self.do_helix_substitute(true, window, cx);
 879    }
 880
 881    fn helix_substitute_no_yank(
 882        &mut self,
 883        _: &HelixSubstituteNoYank,
 884        window: &mut Window,
 885        cx: &mut Context<Self>,
 886    ) {
 887        self.do_helix_substitute(false, window, cx);
 888    }
 889
 890    fn helix_select_next(
 891        &mut self,
 892        _: &HelixSelectNext,
 893        window: &mut Window,
 894        cx: &mut Context<Self>,
 895    ) {
 896        self.do_helix_select(Direction::Next, window, cx);
 897    }
 898
 899    fn helix_select_previous(
 900        &mut self,
 901        _: &HelixSelectPrevious,
 902        window: &mut Window,
 903        cx: &mut Context<Self>,
 904    ) {
 905        self.do_helix_select(Direction::Prev, window, cx);
 906    }
 907
 908    fn do_helix_select(
 909        &mut self,
 910        direction: searchable::Direction,
 911        window: &mut Window,
 912        cx: &mut Context<Self>,
 913    ) {
 914        let Some(pane) = self.pane(window, cx) else {
 915            return;
 916        };
 917        let count = Vim::take_count(cx).unwrap_or(1);
 918        Vim::take_forced_motion(cx);
 919        let prior_selections = self.editor_selections(window, cx);
 920
 921        let success = pane.update(cx, |pane, cx| {
 922            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 923                return false;
 924            };
 925            search_bar.update(cx, |search_bar, cx| {
 926                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 927                    return false;
 928                }
 929                search_bar.select_match(direction, count, window, cx);
 930                true
 931            })
 932        });
 933
 934        if !success {
 935            return;
 936        }
 937        if self.mode == Mode::HelixSelect {
 938            self.update_editor(cx, |_vim, editor, cx| {
 939                let snapshot = editor.snapshot(window, cx);
 940                editor.change_selections(SelectionEffects::default(), window, cx, |s| {
 941                    let buffer = snapshot.buffer_snapshot();
 942
 943                    s.select_anchor_ranges(
 944                        prior_selections
 945                            .iter()
 946                            .cloned()
 947                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
 948                            .sorted_by(|a, b| {
 949                                a.start
 950                                    .cmp(&b.start, buffer)
 951                                    .then_with(|| a.end.cmp(&b.end, buffer))
 952                            })
 953                            .dedup_by(|a, b| {
 954                                a.start.cmp(&b.start, buffer).is_eq()
 955                                    && a.end.cmp(&b.end, buffer).is_eq()
 956                            }),
 957                    );
 958                })
 959            });
 960        }
 961    }
 962}
 963
 964#[cfg(test)]
 965mod test {
 966    use gpui::{KeyBinding, UpdateGlobal, VisualTestContext};
 967    use indoc::indoc;
 968    use project::FakeFs;
 969    use search::{ProjectSearchView, project_search};
 970    use serde_json::json;
 971    use settings::SettingsStore;
 972    use util::path;
 973    use workspace::{DeploySearch, MultiWorkspace};
 974
 975    use crate::{VimAddon, state::Mode, test::VimTestContext};
 976
 977    #[gpui::test]
 978    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
 979        let mut cx = VimTestContext::new(cx, true).await;
 980        cx.enable_helix();
 981        // «
 982        // ˇ
 983        // »
 984        cx.set_state(
 985            indoc! {"
 986            Th«e quiˇ»ck brown
 987            fox jumps over
 988            the lazy dog."},
 989            Mode::HelixNormal,
 990        );
 991
 992        cx.simulate_keystrokes("w");
 993
 994        cx.assert_state(
 995            indoc! {"
 996            The qu«ick ˇ»brown
 997            fox jumps over
 998            the lazy dog."},
 999            Mode::HelixNormal,
1000        );
1001
1002        cx.simulate_keystrokes("w");
1003
1004        cx.assert_state(
1005            indoc! {"
1006            The quick «brownˇ»
1007            fox jumps over
1008            the lazy dog."},
1009            Mode::HelixNormal,
1010        );
1011
1012        cx.simulate_keystrokes("2 b");
1013
1014        cx.assert_state(
1015            indoc! {"
1016            The «ˇquick »brown
1017            fox jumps over
1018            the lazy dog."},
1019            Mode::HelixNormal,
1020        );
1021
1022        cx.simulate_keystrokes("down e up");
1023
1024        cx.assert_state(
1025            indoc! {"
1026            The quicˇk brown
1027            fox jumps over
1028            the lazy dog."},
1029            Mode::HelixNormal,
1030        );
1031
1032        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
1033
1034        cx.simulate_keystroke("b");
1035
1036        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
1037    }
1038
1039    #[gpui::test]
1040    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
1041        let mut cx = VimTestContext::new(cx, true).await;
1042        cx.enable_helix();
1043
1044        // Setup custom keybindings for subword motions so we can use the bindings
1045        // in `simulate_keystroke`.
1046        cx.update(|_window, cx| {
1047            cx.bind_keys([KeyBinding::new(
1048                "w",
1049                crate::motion::NextSubwordStart {
1050                    ignore_punctuation: false,
1051                },
1052                None,
1053            )]);
1054        });
1055
1056        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1057        cx.simulate_keystroke("w");
1058        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1059        cx.simulate_keystroke("w");
1060        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1061        cx.simulate_keystroke("w");
1062        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1063
1064        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1065        cx.simulate_keystroke("w");
1066        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1067        cx.simulate_keystroke("w");
1068        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1069        cx.simulate_keystroke("w");
1070        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1071
1072        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1073        cx.simulate_keystroke("w");
1074        cx.assert_state("«foo_ˇ»bar_baz", Mode::HelixNormal);
1075        cx.simulate_keystroke("w");
1076        cx.assert_state("foo_«bar_ˇ»baz", Mode::HelixNormal);
1077
1078        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1079        cx.simulate_keystroke("w");
1080        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1081        cx.simulate_keystroke("w");
1082        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1083
1084        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1085        cx.simulate_keystroke("w");
1086        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1087        cx.simulate_keystroke("w");
1088        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1089        cx.simulate_keystroke("w");
1090        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1091
1092        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1093        cx.simulate_keystroke("w");
1094        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1095        cx.simulate_keystroke("w");
1096        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1097        cx.simulate_keystroke("w");
1098        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1099        cx.simulate_keystroke("w");
1100        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1101        cx.simulate_keystroke("w");
1102        cx.assert_state("<?php\n\n$some«Variable ˇ»= 2;", Mode::HelixNormal);
1103        cx.simulate_keystroke("w");
1104        cx.assert_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    }
1110
1111    #[gpui::test]
1112    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
1113        let mut cx = VimTestContext::new(cx, true).await;
1114        cx.enable_helix();
1115
1116        // Setup custom keybindings for subword motions so we can use the bindings
1117        // in `simulate_keystroke`.
1118        cx.update(|_window, cx| {
1119            cx.bind_keys([KeyBinding::new(
1120                "e",
1121                crate::motion::NextSubwordEnd {
1122                    ignore_punctuation: false,
1123                },
1124                None,
1125            )]);
1126        });
1127
1128        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1129        cx.simulate_keystroke("e");
1130        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1131        cx.simulate_keystroke("e");
1132        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1133        cx.simulate_keystroke("e");
1134        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1135
1136        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1137        cx.simulate_keystroke("e");
1138        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1139        cx.simulate_keystroke("e");
1140        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1141        cx.simulate_keystroke("e");
1142        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1143
1144        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1145        cx.simulate_keystroke("e");
1146        cx.assert_state("«fooˇ»_bar_baz", Mode::HelixNormal);
1147        cx.simulate_keystroke("e");
1148        cx.assert_state("foo«_barˇ»_baz", Mode::HelixNormal);
1149        cx.simulate_keystroke("e");
1150        cx.assert_state("foo_bar«_bazˇ»", Mode::HelixNormal);
1151
1152        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1153        cx.simulate_keystroke("e");
1154        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1155        cx.simulate_keystroke("e");
1156        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1157        cx.simulate_keystroke("e");
1158        cx.assert_state("fooBar«Bazˇ»", Mode::HelixNormal);
1159
1160        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1161        cx.simulate_keystroke("e");
1162        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1163        cx.simulate_keystroke("e");
1164        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1165        cx.simulate_keystroke("e");
1166        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1167
1168        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1169        cx.simulate_keystroke("e");
1170        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1171        cx.simulate_keystroke("e");
1172        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1173        cx.simulate_keystroke("e");
1174        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1175        cx.simulate_keystroke("e");
1176        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1177        cx.simulate_keystroke("e");
1178        cx.assert_state("<?php\n\n$some«Variableˇ» = 2;", Mode::HelixNormal);
1179        cx.simulate_keystroke("e");
1180        cx.assert_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    }
1186
1187    #[gpui::test]
1188    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
1189        let mut cx = VimTestContext::new(cx, true).await;
1190        cx.enable_helix();
1191
1192        // Setup custom keybindings for subword motions so we can use the bindings
1193        // in `simulate_keystroke`.
1194        cx.update(|_window, cx| {
1195            cx.bind_keys([KeyBinding::new(
1196                "b",
1197                crate::motion::PreviousSubwordStart {
1198                    ignore_punctuation: false,
1199                },
1200                None,
1201            )]);
1202        });
1203
1204        cx.set_state("foo.barˇ", Mode::HelixNormal);
1205        cx.simulate_keystroke("b");
1206        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1207        cx.simulate_keystroke("b");
1208        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1209        cx.simulate_keystroke("b");
1210        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1211
1212        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1213        cx.simulate_keystroke("b");
1214        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1215        cx.simulate_keystroke("b");
1216        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1217        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
1222        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1223        cx.simulate_keystroke("b");
1224        cx.assert_state("foo_bar_«ˇbaz»", Mode::HelixNormal);
1225        cx.simulate_keystroke("b");
1226        cx.assert_state("foo_«ˇbar_»baz", Mode::HelixNormal);
1227        cx.simulate_keystroke("b");
1228        cx.assert_state("«ˇfoo_»bar_baz", Mode::HelixNormal);
1229
1230        cx.set_state("foo;barˇ", Mode::HelixNormal);
1231        cx.simulate_keystroke("b");
1232        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1233        cx.simulate_keystroke("b");
1234        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1235        cx.simulate_keystroke("b");
1236        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1237
1238        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1239        cx.simulate_keystroke("b");
1240        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1241        cx.simulate_keystroke("b");
1242        cx.assert_state("<?php\n\n$someVariable = «ˇ2»;", Mode::HelixNormal);
1243        cx.simulate_keystroke("b");
1244        cx.assert_state("<?php\n\n$someVariable «ˇ= »2;", Mode::HelixNormal);
1245        cx.simulate_keystroke("b");
1246        cx.assert_state("<?php\n\n$some«ˇVariable »= 2;", Mode::HelixNormal);
1247        cx.simulate_keystroke("b");
1248        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1249        cx.simulate_keystroke("b");
1250        cx.assert_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
1256        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1257        cx.simulate_keystroke("b");
1258        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1259        cx.simulate_keystroke("b");
1260        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1261        cx.simulate_keystroke("b");
1262        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1263    }
1264
1265    #[gpui::test]
1266    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
1267        let mut cx = VimTestContext::new(cx, true).await;
1268        cx.enable_helix();
1269
1270        // Setup custom keybindings for subword motions so we can use the bindings
1271        // in `simulate_keystrokes`.
1272        cx.update(|_window, cx| {
1273            cx.bind_keys([KeyBinding::new(
1274                "g e",
1275                crate::motion::PreviousSubwordEnd {
1276                    ignore_punctuation: false,
1277                },
1278                None,
1279            )]);
1280        });
1281
1282        cx.set_state("foo.barˇ", Mode::HelixNormal);
1283        cx.simulate_keystrokes("g e");
1284        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1285        cx.simulate_keystrokes("g e");
1286        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1287        cx.simulate_keystrokes("g e");
1288        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1289
1290        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1291        cx.simulate_keystrokes("g e");
1292        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1293        cx.simulate_keystrokes("g e");
1294        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1295        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
1300        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1301        cx.simulate_keystrokes("g e");
1302        cx.assert_state("foo_bar«ˇ_baz»", Mode::HelixNormal);
1303        cx.simulate_keystrokes("g e");
1304        cx.assert_state("foo«ˇ_bar»_baz", Mode::HelixNormal);
1305        cx.simulate_keystrokes("g e");
1306        cx.assert_state("«ˇfoo»_bar_baz", Mode::HelixNormal);
1307
1308        cx.set_state("foo;barˇ", Mode::HelixNormal);
1309        cx.simulate_keystrokes("g e");
1310        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1311        cx.simulate_keystrokes("g e");
1312        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1313        cx.simulate_keystrokes("g e");
1314        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1315
1316        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1317        cx.simulate_keystrokes("g e");
1318        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1319        cx.simulate_keystrokes("g e");
1320        cx.assert_state("<?php\n\n$someVariable =«ˇ 2»;", Mode::HelixNormal);
1321        cx.simulate_keystrokes("g e");
1322        cx.assert_state("<?php\n\n$someVariable«ˇ =» 2;", Mode::HelixNormal);
1323        cx.simulate_keystrokes("g e");
1324        cx.assert_state("<?php\n\n$some«ˇVariable» = 2;", Mode::HelixNormal);
1325        cx.simulate_keystrokes("g e");
1326        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1327        cx.simulate_keystrokes("g e");
1328        cx.assert_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
1334        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1335        cx.simulate_keystrokes("g e");
1336        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1337        cx.simulate_keystrokes("g e");
1338        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1339        cx.simulate_keystrokes("g e");
1340        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1341    }
1342
1343    #[gpui::test]
1344    async fn test_delete(cx: &mut gpui::TestAppContext) {
1345        let mut cx = VimTestContext::new(cx, true).await;
1346        cx.enable_helix();
1347
1348        // test delete a selection
1349        cx.set_state(
1350            indoc! {"
1351            The qu«ick ˇ»brown
1352            fox jumps over
1353            the lazy dog."},
1354            Mode::HelixNormal,
1355        );
1356
1357        cx.simulate_keystrokes("d");
1358
1359        cx.assert_state(
1360            indoc! {"
1361            The quˇbrown
1362            fox jumps over
1363            the lazy dog."},
1364            Mode::HelixNormal,
1365        );
1366
1367        // test deleting a single character
1368        cx.simulate_keystrokes("d");
1369
1370        cx.assert_state(
1371            indoc! {"
1372            The quˇrown
1373            fox jumps over
1374            the lazy dog."},
1375            Mode::HelixNormal,
1376        );
1377    }
1378
1379    #[gpui::test]
1380    async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
1381        let mut cx = VimTestContext::new(cx, true).await;
1382
1383        cx.set_state(
1384            indoc! {"
1385            The quick brownˇ
1386            fox jumps over
1387            the lazy dog."},
1388            Mode::HelixNormal,
1389        );
1390
1391        cx.simulate_keystrokes("d");
1392
1393        cx.assert_state(
1394            indoc! {"
1395            The quick brownˇfox jumps over
1396            the lazy dog."},
1397            Mode::HelixNormal,
1398        );
1399    }
1400
1401    // #[gpui::test]
1402    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1403    //     let mut cx = VimTestContext::new(cx, true).await;
1404
1405    //     cx.set_state(
1406    //         indoc! {"
1407    //         The quick brown
1408    //         fox jumps over
1409    //         the lazy dog.ˇ"},
1410    //         Mode::HelixNormal,
1411    //     );
1412
1413    //     cx.simulate_keystrokes("d");
1414
1415    //     cx.assert_state(
1416    //         indoc! {"
1417    //         The quick brown
1418    //         fox jumps over
1419    //         the lazy dog.ˇ"},
1420    //         Mode::HelixNormal,
1421    //     );
1422    // }
1423
1424    #[gpui::test]
1425    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1426        let mut cx = VimTestContext::new(cx, true).await;
1427        cx.enable_helix();
1428
1429        cx.set_state(
1430            indoc! {"
1431            The quˇick brown
1432            fox jumps over
1433            the lazy dog."},
1434            Mode::HelixNormal,
1435        );
1436
1437        cx.simulate_keystrokes("f z");
1438
1439        cx.assert_state(
1440            indoc! {"
1441                The qu«ick brown
1442                fox jumps over
1443                the lazˇ»y dog."},
1444            Mode::HelixNormal,
1445        );
1446
1447        cx.simulate_keystrokes("F e F e");
1448
1449        cx.assert_state(
1450            indoc! {"
1451                The quick brown
1452                fox jumps ov«ˇer
1453                the» lazy dog."},
1454            Mode::HelixNormal,
1455        );
1456
1457        cx.simulate_keystrokes("e 2 F e");
1458
1459        cx.assert_state(
1460            indoc! {"
1461                Th«ˇe quick brown
1462                fox jumps over»
1463                the lazy dog."},
1464            Mode::HelixNormal,
1465        );
1466
1467        cx.simulate_keystrokes("t r t r");
1468
1469        cx.assert_state(
1470            indoc! {"
1471                The quick «brown
1472                fox jumps oveˇ»r
1473                the lazy dog."},
1474            Mode::HelixNormal,
1475        );
1476    }
1477
1478    #[gpui::test]
1479    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1480        let mut cx = VimTestContext::new(cx, true).await;
1481        cx.enable_helix();
1482
1483        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1484
1485        cx.simulate_keystroke("w");
1486
1487        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1488
1489        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1490
1491        cx.simulate_keystroke("b");
1492
1493        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1494    }
1495
1496    #[gpui::test]
1497    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1498        let mut cx = VimTestContext::new(cx, true).await;
1499        cx.enable_helix();
1500        cx.set_state(
1501            indoc! {"
1502            «The ˇ»quick brown
1503            fox jumps over
1504            the lazy dog."},
1505            Mode::HelixNormal,
1506        );
1507
1508        cx.simulate_keystrokes("i");
1509
1510        cx.assert_state(
1511            indoc! {"
1512            ˇThe quick brown
1513            fox jumps over
1514            the lazy dog."},
1515            Mode::Insert,
1516        );
1517    }
1518
1519    #[gpui::test]
1520    async fn test_append(cx: &mut gpui::TestAppContext) {
1521        let mut cx = VimTestContext::new(cx, true).await;
1522        cx.enable_helix();
1523        // test from the end of the selection
1524        cx.set_state(
1525            indoc! {"
1526            «Theˇ» quick brown
1527            fox jumps over
1528            the lazy dog."},
1529            Mode::HelixNormal,
1530        );
1531
1532        cx.simulate_keystrokes("a");
1533
1534        cx.assert_state(
1535            indoc! {"
1536            Theˇ quick brown
1537            fox jumps over
1538            the lazy dog."},
1539            Mode::Insert,
1540        );
1541
1542        // test from the beginning of the selection
1543        cx.set_state(
1544            indoc! {"
1545            «ˇThe» quick brown
1546            fox jumps over
1547            the lazy dog."},
1548            Mode::HelixNormal,
1549        );
1550
1551        cx.simulate_keystrokes("a");
1552
1553        cx.assert_state(
1554            indoc! {"
1555            Theˇ quick brown
1556            fox jumps over
1557            the lazy dog."},
1558            Mode::Insert,
1559        );
1560    }
1561
1562    #[gpui::test]
1563    async fn test_replace(cx: &mut gpui::TestAppContext) {
1564        let mut cx = VimTestContext::new(cx, true).await;
1565        cx.enable_helix();
1566
1567        // No selection (single character)
1568        cx.set_state("ˇaa", Mode::HelixNormal);
1569
1570        cx.simulate_keystrokes("r x");
1571
1572        cx.assert_state("ˇxa", Mode::HelixNormal);
1573
1574        // Cursor at the beginning
1575        cx.set_state("«ˇaa»", Mode::HelixNormal);
1576
1577        cx.simulate_keystrokes("r x");
1578
1579        cx.assert_state("«ˇxx»", Mode::HelixNormal);
1580
1581        // Cursor at the end
1582        cx.set_state("«aaˇ»", Mode::HelixNormal);
1583
1584        cx.simulate_keystrokes("r x");
1585
1586        cx.assert_state("«xxˇ»", Mode::HelixNormal);
1587    }
1588
1589    #[gpui::test]
1590    async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1591        let mut cx = VimTestContext::new(cx, true).await;
1592        cx.enable_helix();
1593
1594        // Test yanking current character with no selection
1595        cx.set_state("hello ˇworld", Mode::HelixNormal);
1596        cx.simulate_keystrokes("y");
1597
1598        // Test cursor remains at the same position after yanking single character
1599        cx.assert_state("hello ˇworld", Mode::HelixNormal);
1600        cx.shared_clipboard().assert_eq("w");
1601
1602        // Move cursor and yank another character
1603        cx.simulate_keystrokes("l");
1604        cx.simulate_keystrokes("y");
1605        cx.shared_clipboard().assert_eq("o");
1606
1607        // Test yanking with existing selection
1608        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1609        cx.simulate_keystrokes("y");
1610        cx.shared_clipboard().assert_eq("worl");
1611        cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1612
1613        // Test yanking in select mode character by character
1614        cx.set_state("hello ˇworld", Mode::HelixNormal);
1615        cx.simulate_keystroke("v");
1616        cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1617        cx.simulate_keystroke("y");
1618        cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1619        cx.shared_clipboard().assert_eq("w");
1620    }
1621
1622    #[gpui::test]
1623    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1624        let mut cx = VimTestContext::new(cx, true).await;
1625        cx.enable_helix();
1626
1627        // First copy some text to clipboard
1628        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1629        cx.simulate_keystrokes("y");
1630
1631        // Test paste with shift-r on single cursor
1632        cx.set_state("foo ˇbar", Mode::HelixNormal);
1633        cx.simulate_keystrokes("shift-r");
1634
1635        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1636
1637        // Test paste with shift-r on selection
1638        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1639        cx.simulate_keystrokes("shift-r");
1640
1641        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1642    }
1643
1644    #[gpui::test]
1645    async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1646        let mut cx = VimTestContext::new(cx, true).await;
1647
1648        assert_eq!(cx.mode(), Mode::Normal);
1649        cx.enable_helix();
1650
1651        cx.simulate_keystrokes("v");
1652        assert_eq!(cx.mode(), Mode::HelixSelect);
1653        cx.simulate_keystrokes("escape");
1654        assert_eq!(cx.mode(), Mode::HelixNormal);
1655    }
1656
1657    #[gpui::test]
1658    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1659        let mut cx = VimTestContext::new(cx, true).await;
1660        cx.enable_helix();
1661
1662        // Make a modification at a specific location
1663        cx.set_state("ˇhello", Mode::HelixNormal);
1664        assert_eq!(cx.mode(), Mode::HelixNormal);
1665        cx.simulate_keystrokes("i");
1666        assert_eq!(cx.mode(), Mode::Insert);
1667        cx.simulate_keystrokes("escape");
1668        assert_eq!(cx.mode(), Mode::HelixNormal);
1669    }
1670
1671    #[gpui::test]
1672    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1673        let mut cx = VimTestContext::new(cx, true).await;
1674        cx.enable_helix();
1675
1676        // Make a modification at a specific location
1677        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1678        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1679        cx.simulate_keystrokes("i");
1680        cx.simulate_keystrokes("escape");
1681        cx.simulate_keystrokes("i");
1682        cx.simulate_keystrokes("m o d i f i e d space");
1683        cx.simulate_keystrokes("escape");
1684
1685        // TODO: this fails, because state is no longer helix
1686        cx.assert_state(
1687            "line one\nline modified ˇtwo\nline three",
1688            Mode::HelixNormal,
1689        );
1690
1691        // Move cursor away from the modification
1692        cx.simulate_keystrokes("up");
1693
1694        // Use "g ." to go back to last modification
1695        cx.simulate_keystrokes("g .");
1696
1697        // Verify we're back at the modification location and still in HelixNormal mode
1698        cx.assert_state(
1699            "line one\nline modifiedˇ two\nline three",
1700            Mode::HelixNormal,
1701        );
1702    }
1703
1704    #[gpui::test]
1705    async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1706        let mut cx = VimTestContext::new(cx, true).await;
1707        cx.set_state(
1708            "line one\nline ˇtwo\nline three\nline four",
1709            Mode::HelixNormal,
1710        );
1711        cx.simulate_keystrokes("2 x");
1712        cx.assert_state(
1713            "line one\n«line two\nline three\nˇ»line four",
1714            Mode::HelixNormal,
1715        );
1716
1717        // Test extending existing line selection
1718        cx.set_state(
1719            indoc! {"
1720            li«ˇne one
1721            li»ne two
1722            line three
1723            line four"},
1724            Mode::HelixNormal,
1725        );
1726        cx.simulate_keystrokes("x");
1727        cx.assert_state(
1728            indoc! {"
1729            «line one
1730            line two
1731            ˇ»line three
1732            line four"},
1733            Mode::HelixNormal,
1734        );
1735
1736        // Pressing x in empty line, select next line (because helix considers cursor a selection)
1737        cx.set_state(
1738            indoc! {"
1739            line one
1740            ˇ
1741            line three
1742            line four
1743            line five
1744            line six"},
1745            Mode::HelixNormal,
1746        );
1747        cx.simulate_keystrokes("x");
1748        cx.assert_state(
1749            indoc! {"
1750            line one
1751            «
1752            line three
1753            ˇ»line four
1754            line five
1755            line six"},
1756            Mode::HelixNormal,
1757        );
1758
1759        // Another x should only select the next line
1760        cx.simulate_keystrokes("x");
1761        cx.assert_state(
1762            indoc! {"
1763            line one
1764            «
1765            line three
1766            line four
1767            ˇ»line five
1768            line six"},
1769            Mode::HelixNormal,
1770        );
1771
1772        // Empty line with count selects extra + count lines
1773        cx.set_state(
1774            indoc! {"
1775            line one
1776            ˇ
1777            line three
1778            line four
1779            line five"},
1780            Mode::HelixNormal,
1781        );
1782        cx.simulate_keystrokes("2 x");
1783        cx.assert_state(
1784            indoc! {"
1785            line one
1786            «
1787            line three
1788            line four
1789            ˇ»line five"},
1790            Mode::HelixNormal,
1791        );
1792
1793        // Compare empty vs non-empty line behavior
1794        cx.set_state(
1795            indoc! {"
1796            ˇnon-empty line
1797            line two
1798            line three"},
1799            Mode::HelixNormal,
1800        );
1801        cx.simulate_keystrokes("x");
1802        cx.assert_state(
1803            indoc! {"
1804            «non-empty line
1805            ˇ»line two
1806            line three"},
1807            Mode::HelixNormal,
1808        );
1809
1810        // Same test but with empty line - should select one extra
1811        cx.set_state(
1812            indoc! {"
1813            ˇ
1814            line two
1815            line three"},
1816            Mode::HelixNormal,
1817        );
1818        cx.simulate_keystrokes("x");
1819        cx.assert_state(
1820            indoc! {"
1821            «
1822            line two
1823            ˇ»line three"},
1824            Mode::HelixNormal,
1825        );
1826
1827        // Test selecting multiple lines with count
1828        cx.set_state(
1829            indoc! {"
1830            ˇline one
1831            line two
1832            line threeˇ
1833            line four
1834            line five"},
1835            Mode::HelixNormal,
1836        );
1837        cx.simulate_keystrokes("x");
1838        cx.assert_state(
1839            indoc! {"
1840            «line one
1841            ˇ»line two
1842            «line three
1843            ˇ»line four
1844            line five"},
1845            Mode::HelixNormal,
1846        );
1847        cx.simulate_keystrokes("x");
1848        // Adjacent line selections stay separate (not merged)
1849        cx.assert_state(
1850            indoc! {"
1851            «line one
1852            line two
1853            ˇ»«line three
1854            line four
1855            ˇ»line five"},
1856            Mode::HelixNormal,
1857        );
1858
1859        // Test selecting with an empty line below the current line
1860        cx.set_state(
1861            indoc! {"
1862            line one
1863            line twoˇ
1864
1865            line four
1866            line five"},
1867            Mode::HelixNormal,
1868        );
1869        cx.simulate_keystrokes("x");
1870        cx.assert_state(
1871            indoc! {"
1872            line one
1873            «line two
1874            ˇ»
1875            line four
1876            line five"},
1877            Mode::HelixNormal,
1878        );
1879        cx.simulate_keystrokes("x");
1880        cx.assert_state(
1881            indoc! {"
1882            line one
1883            «line two
1884
1885            ˇ»line four
1886            line five"},
1887            Mode::HelixNormal,
1888        );
1889        cx.simulate_keystrokes("x");
1890        cx.assert_state(
1891            indoc! {"
1892            line one
1893            «line two
1894
1895            line four
1896            ˇ»line five"},
1897            Mode::HelixNormal,
1898        );
1899    }
1900
1901    #[gpui::test]
1902    async fn test_helix_insert_before_after_select_lines(cx: &mut gpui::TestAppContext) {
1903        let mut cx = VimTestContext::new(cx, true).await;
1904
1905        cx.set_state(
1906            "line one\nline ˇtwo\nline three\nline four",
1907            Mode::HelixNormal,
1908        );
1909        cx.simulate_keystrokes("2 x");
1910        cx.assert_state(
1911            "line one\n«line two\nline three\nˇ»line four",
1912            Mode::HelixNormal,
1913        );
1914        cx.simulate_keystrokes("o");
1915        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1916
1917        cx.set_state(
1918            "line one\nline ˇtwo\nline three\nline four",
1919            Mode::HelixNormal,
1920        );
1921        cx.simulate_keystrokes("2 x");
1922        cx.assert_state(
1923            "line one\n«line two\nline three\nˇ»line four",
1924            Mode::HelixNormal,
1925        );
1926        cx.simulate_keystrokes("shift-o");
1927        cx.assert_state("line one\nˇ\nline two\nline three\nline four", Mode::Insert);
1928    }
1929
1930    #[gpui::test]
1931    async fn test_helix_insert_before_after_helix_select(cx: &mut gpui::TestAppContext) {
1932        let mut cx = VimTestContext::new(cx, true).await;
1933        cx.enable_helix();
1934
1935        // Test new line in selection direction
1936        cx.set_state(
1937            "ˇline one\nline two\nline three\nline four",
1938            Mode::HelixNormal,
1939        );
1940        cx.simulate_keystrokes("v j j");
1941        cx.assert_state(
1942            "«line one\nline two\nlˇ»ine three\nline four",
1943            Mode::HelixSelect,
1944        );
1945        cx.simulate_keystrokes("o");
1946        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1947
1948        cx.set_state(
1949            "line one\nline two\nˇline three\nline four",
1950            Mode::HelixNormal,
1951        );
1952        cx.simulate_keystrokes("v k k");
1953        cx.assert_state(
1954            "«ˇline one\nline two\nl»ine three\nline four",
1955            Mode::HelixSelect,
1956        );
1957        cx.simulate_keystrokes("shift-o");
1958        cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1959
1960        // Test new line in opposite selection direction
1961        cx.set_state(
1962            "ˇline one\nline two\nline three\nline four",
1963            Mode::HelixNormal,
1964        );
1965        cx.simulate_keystrokes("v j j");
1966        cx.assert_state(
1967            "«line one\nline two\nlˇ»ine three\nline four",
1968            Mode::HelixSelect,
1969        );
1970        cx.simulate_keystrokes("shift-o");
1971        cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1972
1973        cx.set_state(
1974            "line one\nline two\nˇline three\nline four",
1975            Mode::HelixNormal,
1976        );
1977        cx.simulate_keystrokes("v k k");
1978        cx.assert_state(
1979            "«ˇline one\nline two\nl»ine three\nline four",
1980            Mode::HelixSelect,
1981        );
1982        cx.simulate_keystrokes("o");
1983        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1984    }
1985
1986    #[gpui::test]
1987    async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1988        let mut cx = VimTestContext::new(cx, true).await;
1989
1990        assert_eq!(cx.mode(), Mode::Normal);
1991        cx.enable_helix();
1992
1993        cx.set_state("ˇhello", Mode::HelixNormal);
1994        cx.simulate_keystrokes("l v l l");
1995        cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1996    }
1997
1998    #[gpui::test]
1999    async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
2000        let mut cx = VimTestContext::new(cx, true).await;
2001
2002        assert_eq!(cx.mode(), Mode::Normal);
2003        cx.enable_helix();
2004
2005        // Start with multiple cursors (no selections)
2006        cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
2007
2008        // Enter select mode and move right twice
2009        cx.simulate_keystrokes("v l l");
2010
2011        // Each cursor should independently create and extend its own selection
2012        cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
2013    }
2014
2015    #[gpui::test]
2016    async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
2017        let mut cx = VimTestContext::new(cx, true).await;
2018
2019        cx.set_state("ˇone two", Mode::Normal);
2020        cx.simulate_keystrokes("v w");
2021        cx.assert_state("«one tˇ»wo", Mode::Visual);
2022
2023        // In Vim, this selects "t". In helix selections stops just before "t"
2024
2025        cx.enable_helix();
2026        cx.set_state("ˇone two", Mode::HelixNormal);
2027        cx.simulate_keystrokes("v w");
2028        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2029    }
2030
2031    #[gpui::test]
2032    async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
2033        let mut cx = VimTestContext::new(cx, true).await;
2034
2035        cx.set_state("ˇone two", Mode::Normal);
2036        cx.simulate_keystrokes("v w");
2037        cx.assert_state("«one tˇ»wo", Mode::Visual);
2038        cx.simulate_keystrokes("escape");
2039        cx.assert_state("one ˇtwo", Mode::Normal);
2040
2041        cx.enable_helix();
2042        cx.set_state("ˇone two", Mode::HelixNormal);
2043        cx.simulate_keystrokes("v w");
2044        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2045        cx.simulate_keystrokes("escape");
2046        cx.assert_state("«one ˇ»two", Mode::HelixNormal);
2047    }
2048
2049    #[gpui::test]
2050    async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
2051        let mut cx = VimTestContext::new(cx, true).await;
2052        cx.enable_helix();
2053
2054        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2055        cx.simulate_keystrokes("w");
2056        cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
2057
2058        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2059        cx.simulate_keystrokes("e");
2060        cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
2061    }
2062
2063    #[gpui::test]
2064    async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
2065        let mut cx = VimTestContext::new(cx, true).await;
2066        cx.enable_helix();
2067
2068        cx.set_state("ˇone two three", Mode::HelixNormal);
2069        cx.simulate_keystrokes("l l v h h h");
2070        cx.assert_state("«ˇone» two three", Mode::HelixSelect);
2071    }
2072
2073    #[gpui::test]
2074    async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
2075        let mut cx = VimTestContext::new(cx, true).await;
2076        cx.enable_helix();
2077
2078        cx.set_state("ˇone two one", Mode::HelixNormal);
2079        cx.simulate_keystrokes("x");
2080        cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
2081        cx.simulate_keystrokes("s o n e");
2082        cx.run_until_parked();
2083        cx.simulate_keystrokes("enter");
2084        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2085
2086        cx.simulate_keystrokes("x");
2087        cx.simulate_keystrokes("s");
2088        cx.run_until_parked();
2089        cx.simulate_keystrokes("enter");
2090        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2091
2092        // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
2093        // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
2094        // cx.simulate_keystrokes("s o n e enter");
2095        // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
2096    }
2097
2098    #[gpui::test]
2099    async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
2100        let mut cx = VimTestContext::new(cx, true).await;
2101
2102        cx.set_state("ˇhello two one two one two one", Mode::Visual);
2103        cx.simulate_keystrokes("/ o n e");
2104        cx.simulate_keystrokes("enter");
2105        cx.simulate_keystrokes("n n");
2106        cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
2107
2108        cx.set_state("ˇhello two one two one two one", Mode::Normal);
2109        cx.simulate_keystrokes("/ o n e");
2110        cx.simulate_keystrokes("enter");
2111        cx.simulate_keystrokes("n n");
2112        cx.assert_state("hello two one two one two ˇone", Mode::Normal);
2113
2114        cx.set_state("ˇhello two one two one two one", Mode::Normal);
2115        cx.simulate_keystrokes("/ o n e");
2116        cx.simulate_keystrokes("enter");
2117        cx.simulate_keystrokes("n g n g n");
2118        cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
2119
2120        cx.enable_helix();
2121
2122        cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
2123        cx.simulate_keystrokes("/ o n e");
2124        cx.simulate_keystrokes("enter");
2125        cx.simulate_keystrokes("n n");
2126        cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
2127
2128        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2129        cx.simulate_keystrokes("/ o n e");
2130        cx.simulate_keystrokes("enter");
2131        cx.simulate_keystrokes("n n");
2132        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2133    }
2134
2135    #[gpui::test]
2136    async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
2137        let mut cx = VimTestContext::new(cx, true).await;
2138        cx.enable_helix();
2139
2140        // Three occurrences of "one". After selecting all three with `n n`,
2141        // pressing `n` again wraps the search to the first occurrence.
2142        // The prior selections (at higher offsets) are chained before the
2143        // wrapped selection (at a lower offset), producing unsorted anchors
2144        // that cause `rope::Cursor::summary` to panic with
2145        // "cannot summarize backward".
2146        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2147        cx.simulate_keystrokes("/ o n e");
2148        cx.simulate_keystrokes("enter");
2149        cx.simulate_keystrokes("n n n");
2150        // Should not panic; all three occurrences should remain selected.
2151        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2152    }
2153
2154    #[gpui::test]
2155    async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
2156        let mut cx = VimTestContext::new(cx, true).await;
2157
2158        cx.set_state("ˇone two", Mode::HelixNormal);
2159        cx.simulate_keystrokes("c");
2160        cx.assert_state("ˇne two", Mode::Insert);
2161
2162        cx.set_state("«oneˇ» two", Mode::HelixNormal);
2163        cx.simulate_keystrokes("c");
2164        cx.assert_state("ˇ two", Mode::Insert);
2165
2166        cx.set_state(
2167            indoc! {"
2168            oneˇ two
2169            three
2170            "},
2171            Mode::HelixNormal,
2172        );
2173        cx.simulate_keystrokes("x c");
2174        cx.assert_state(
2175            indoc! {"
2176            ˇ
2177            three
2178            "},
2179            Mode::Insert,
2180        );
2181
2182        cx.set_state(
2183            indoc! {"
2184            one twoˇ
2185            three
2186            "},
2187            Mode::HelixNormal,
2188        );
2189        cx.simulate_keystrokes("c");
2190        cx.assert_state(
2191            indoc! {"
2192            one twoˇthree
2193            "},
2194            Mode::Insert,
2195        );
2196
2197        // Helix doesn't set the cursor to the first non-blank one when
2198        // replacing lines: it uses language-dependent indent queries instead.
2199        cx.set_state(
2200            indoc! {"
2201            one two
2202            «    indented
2203            three not indentedˇ»
2204            "},
2205            Mode::HelixNormal,
2206        );
2207        cx.simulate_keystrokes("c");
2208        cx.set_state(
2209            indoc! {"
2210            one two
2211            ˇ
2212            "},
2213            Mode::Insert,
2214        );
2215    }
2216
2217    #[gpui::test]
2218    async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
2219        let mut cx = VimTestContext::new(cx, true).await;
2220        cx.enable_helix();
2221
2222        // Test g l moves to last character, not after it
2223        cx.set_state("hello ˇworld!", Mode::HelixNormal);
2224        cx.simulate_keystrokes("g l");
2225        cx.assert_state("hello worldˇ!", Mode::HelixNormal);
2226
2227        // Test with Chinese characters, test if work with UTF-8?
2228        cx.set_state("ˇ你好世界", Mode::HelixNormal);
2229        cx.simulate_keystrokes("g l");
2230        cx.assert_state("你好世ˇ界", Mode::HelixNormal);
2231
2232        // Test with end of line
2233        cx.set_state("endˇ", Mode::HelixNormal);
2234        cx.simulate_keystrokes("g l");
2235        cx.assert_state("enˇd", Mode::HelixNormal);
2236
2237        // Test with empty line
2238        cx.set_state(
2239            indoc! {"
2240                hello
2241                ˇ
2242                world"},
2243            Mode::HelixNormal,
2244        );
2245        cx.simulate_keystrokes("g l");
2246        cx.assert_state(
2247            indoc! {"
2248                hello
2249                ˇ
2250                world"},
2251            Mode::HelixNormal,
2252        );
2253
2254        // Test with multiple lines
2255        cx.set_state(
2256            indoc! {"
2257                ˇfirst line
2258                second line
2259                third line"},
2260            Mode::HelixNormal,
2261        );
2262        cx.simulate_keystrokes("g l");
2263        cx.assert_state(
2264            indoc! {"
2265                first linˇe
2266                second line
2267                third line"},
2268            Mode::HelixNormal,
2269        );
2270    }
2271
2272    #[gpui::test]
2273    async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
2274        VimTestContext::init(cx);
2275
2276        let fs = FakeFs::new(cx.background_executor.clone());
2277        fs.insert_tree(
2278            path!("/dir"),
2279            json!({
2280                "file_a.rs": "// File A.",
2281                "file_b.rs": "// File B.",
2282            }),
2283        )
2284        .await;
2285
2286        let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2287        let window_handle =
2288            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2289        let workspace = window_handle
2290            .read_with(cx, |mw, _| mw.workspace().clone())
2291            .unwrap();
2292
2293        cx.update(|cx| {
2294            VimTestContext::init_keybindings(true, cx);
2295            SettingsStore::update_global(cx, |store, cx| {
2296                store.update_user_settings(cx, |store| store.helix_mode = Some(true));
2297            })
2298        });
2299
2300        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2301
2302        workspace.update_in(cx, |workspace, window, cx| {
2303            ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
2304        });
2305
2306        let search_view = workspace.update_in(cx, |workspace, _, cx| {
2307            workspace
2308                .active_pane()
2309                .read(cx)
2310                .items()
2311                .find_map(|item| item.downcast::<ProjectSearchView>())
2312                .expect("Project search view should be active")
2313        });
2314
2315        project_search::perform_project_search(&search_view, "File A", cx);
2316
2317        search_view.update(cx, |search_view, cx| {
2318            let vim_mode = search_view
2319                .results_editor()
2320                .read(cx)
2321                .addon::<VimAddon>()
2322                .map(|addon| addon.entity.read(cx).mode);
2323
2324            assert_eq!(vim_mode, Some(Mode::HelixNormal));
2325        });
2326    }
2327
2328    #[gpui::test]
2329    async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
2330        let mut cx = VimTestContext::new(cx, true).await;
2331        cx.enable_helix();
2332
2333        // Start with a selection
2334        cx.set_state(
2335            indoc! {"
2336            «lineˇ» one
2337            line two
2338            line three
2339            line four
2340            line five"},
2341            Mode::HelixNormal,
2342        );
2343
2344        // Scroll down, selection should collapse
2345        cx.simulate_keystrokes("ctrl-d");
2346        cx.assert_state(
2347            indoc! {"
2348            line one
2349            line two
2350            line three
2351            line four
2352            line fiveˇ"},
2353            Mode::HelixNormal,
2354        );
2355
2356        // Make a new selection
2357        cx.simulate_keystroke("b");
2358        cx.assert_state(
2359            indoc! {"
2360            line one
2361            line two
2362            line three
2363            line four
2364            line «ˇfive»"},
2365            Mode::HelixNormal,
2366        );
2367
2368        // And scroll up, once again collapsing the selection.
2369        cx.simulate_keystroke("ctrl-u");
2370        cx.assert_state(
2371            indoc! {"
2372            line one
2373            line two
2374            line three
2375            line ˇfour
2376            line five"},
2377            Mode::HelixNormal,
2378        );
2379
2380        // Enter select mode
2381        cx.simulate_keystroke("v");
2382        cx.assert_state(
2383            indoc! {"
2384            line one
2385            line two
2386            line three
2387            line «fˇ»our
2388            line five"},
2389            Mode::HelixSelect,
2390        );
2391
2392        // And now the selection should be kept/expanded.
2393        cx.simulate_keystroke("ctrl-d");
2394        cx.assert_state(
2395            indoc! {"
2396            line one
2397            line two
2398            line three
2399            line «four
2400            line fiveˇ»"},
2401            Mode::HelixSelect,
2402        );
2403    }
2404
2405    #[gpui::test]
2406    async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
2407        let mut cx = VimTestContext::new(cx, true).await;
2408        cx.enable_helix();
2409
2410        // Ensure that, when lines are selected using `x`, pressing `shift-a`
2411        // actually puts the cursor at the end of the selected lines and not at
2412        // the end of the line below.
2413        cx.set_state(
2414            indoc! {"
2415            line oˇne
2416            line two"},
2417            Mode::HelixNormal,
2418        );
2419
2420        cx.simulate_keystrokes("x");
2421        cx.assert_state(
2422            indoc! {"
2423            «line one
2424            ˇ»line two"},
2425            Mode::HelixNormal,
2426        );
2427
2428        cx.simulate_keystrokes("shift-a");
2429        cx.assert_state(
2430            indoc! {"
2431            line oneˇ
2432            line two"},
2433            Mode::Insert,
2434        );
2435
2436        cx.set_state(
2437            indoc! {"
2438            line «one
2439            lineˇ» two"},
2440            Mode::HelixNormal,
2441        );
2442
2443        cx.simulate_keystrokes("shift-a");
2444        cx.assert_state(
2445            indoc! {"
2446            line one
2447            line twoˇ"},
2448            Mode::Insert,
2449        );
2450    }
2451
2452    #[gpui::test]
2453    async fn test_helix_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2454        let mut cx = VimTestContext::new(cx, true).await;
2455        cx.enable_helix();
2456
2457        cx.set_state("«Hällöˇ» Wörld", Mode::HelixNormal);
2458        cx.simulate_keystrokes("r 1");
2459        cx.assert_state("«11111ˇ» Wörld", Mode::HelixNormal);
2460
2461        cx.set_state("«e\u{301}ˇ»", Mode::HelixNormal);
2462        cx.simulate_keystrokes("r 1");
2463        cx.assert_state("«1ˇ»", Mode::HelixNormal);
2464
2465        cx.set_state("«🙂ˇ»", Mode::HelixNormal);
2466        cx.simulate_keystrokes("r 1");
2467        cx.assert_state("«1ˇ»", Mode::HelixNormal);
2468    }
2469}