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