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