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