helix.rs

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