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