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') && selection.is_empty() {
 718                    1
 719                } else {
 720                    0
 721                };
 722
 723                let end_row = current_end_row + count as u32 + extra_line;
 724
 725                selection.start = Point::new(start_row, 0);
 726                selection.end = if end_row > max_point.row {
 727                    max_point
 728                } else {
 729                    Point::new(end_row, 0)
 730                };
 731                selection.reversed = false;
 732            }
 733
 734            editor.change_selections(Default::default(), window, cx, |s| {
 735                s.select(selections);
 736            });
 737        });
 738    }
 739
 740    fn helix_keep_newest_selection(
 741        &mut self,
 742        _: &HelixKeepNewestSelection,
 743        window: &mut Window,
 744        cx: &mut Context<Self>,
 745    ) {
 746        self.update_editor(cx, |_, editor, cx| {
 747            let newest = editor
 748                .selections
 749                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
 750            editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
 751        });
 752    }
 753
 754    fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
 755        self.update_editor(cx, |vim, editor, cx| {
 756            editor.set_clip_at_line_ends(false, cx);
 757            editor.transact(window, cx, |editor, window, cx| {
 758                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 759                    s.move_with(|map, selection| {
 760                        if selection.start == selection.end {
 761                            selection.end = movement::right(map, selection.end);
 762                        }
 763
 764                        // If the selection starts and ends on a newline, we exclude the last one.
 765                        if !selection.is_empty()
 766                            && selection.start.column() == 0
 767                            && selection.end.column() == 0
 768                        {
 769                            selection.end = movement::left(map, selection.end);
 770                        }
 771                    })
 772                });
 773                if yank {
 774                    vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
 775                }
 776                let selections = editor
 777                    .selections
 778                    .all::<Point>(&editor.display_snapshot(cx))
 779                    .into_iter();
 780                let edits = selections.map(|selection| (selection.start..selection.end, ""));
 781                editor.edit(edits, cx);
 782            });
 783        });
 784        self.switch_mode(Mode::Insert, true, window, cx);
 785    }
 786
 787    fn helix_substitute(
 788        &mut self,
 789        _: &HelixSubstitute,
 790        window: &mut Window,
 791        cx: &mut Context<Self>,
 792    ) {
 793        self.do_helix_substitute(true, window, cx);
 794    }
 795
 796    fn helix_substitute_no_yank(
 797        &mut self,
 798        _: &HelixSubstituteNoYank,
 799        window: &mut Window,
 800        cx: &mut Context<Self>,
 801    ) {
 802        self.do_helix_substitute(false, window, cx);
 803    }
 804
 805    fn helix_select_next(
 806        &mut self,
 807        _: &HelixSelectNext,
 808        window: &mut Window,
 809        cx: &mut Context<Self>,
 810    ) {
 811        self.do_helix_select(Direction::Next, window, cx);
 812    }
 813
 814    fn helix_select_previous(
 815        &mut self,
 816        _: &HelixSelectPrevious,
 817        window: &mut Window,
 818        cx: &mut Context<Self>,
 819    ) {
 820        self.do_helix_select(Direction::Prev, window, cx);
 821    }
 822
 823    fn do_helix_select(
 824        &mut self,
 825        direction: searchable::Direction,
 826        window: &mut Window,
 827        cx: &mut Context<Self>,
 828    ) {
 829        let Some(pane) = self.pane(window, cx) else {
 830            return;
 831        };
 832        let count = Vim::take_count(cx).unwrap_or(1);
 833        Vim::take_forced_motion(cx);
 834        let prior_selections = self.editor_selections(window, cx);
 835
 836        let success = pane.update(cx, |pane, cx| {
 837            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 838                return false;
 839            };
 840            search_bar.update(cx, |search_bar, cx| {
 841                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 842                    return false;
 843                }
 844                search_bar.select_match(direction, count, window, cx);
 845                true
 846            })
 847        });
 848
 849        if !success {
 850            return;
 851        }
 852        if self.mode == Mode::HelixSelect {
 853            self.update_editor(cx, |_vim, editor, cx| {
 854                let snapshot = editor.snapshot(window, cx);
 855                editor.change_selections(SelectionEffects::default(), window, cx, |s| {
 856                    s.select_anchor_ranges(
 857                        prior_selections
 858                            .iter()
 859                            .cloned()
 860                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())),
 861                    );
 862                })
 863            });
 864        }
 865    }
 866}
 867
 868#[cfg(test)]
 869mod test {
 870    use gpui::{UpdateGlobal, VisualTestContext};
 871    use indoc::indoc;
 872    use project::FakeFs;
 873    use search::{ProjectSearchView, project_search};
 874    use serde_json::json;
 875    use settings::SettingsStore;
 876    use util::path;
 877    use workspace::DeploySearch;
 878
 879    use crate::{VimAddon, state::Mode, test::VimTestContext};
 880
 881    #[gpui::test]
 882    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
 883        let mut cx = VimTestContext::new(cx, true).await;
 884        cx.enable_helix();
 885        // «
 886        // ˇ
 887        // »
 888        cx.set_state(
 889            indoc! {"
 890            Th«e quiˇ»ck brown
 891            fox jumps over
 892            the lazy dog."},
 893            Mode::HelixNormal,
 894        );
 895
 896        cx.simulate_keystrokes("w");
 897
 898        cx.assert_state(
 899            indoc! {"
 900            The qu«ick ˇ»brown
 901            fox jumps over
 902            the lazy dog."},
 903            Mode::HelixNormal,
 904        );
 905
 906        cx.simulate_keystrokes("w");
 907
 908        cx.assert_state(
 909            indoc! {"
 910            The quick «brownˇ»
 911            fox jumps over
 912            the lazy dog."},
 913            Mode::HelixNormal,
 914        );
 915
 916        cx.simulate_keystrokes("2 b");
 917
 918        cx.assert_state(
 919            indoc! {"
 920            The «ˇquick »brown
 921            fox jumps over
 922            the lazy dog."},
 923            Mode::HelixNormal,
 924        );
 925
 926        cx.simulate_keystrokes("down e up");
 927
 928        cx.assert_state(
 929            indoc! {"
 930            The quicˇk brown
 931            fox jumps over
 932            the lazy dog."},
 933            Mode::HelixNormal,
 934        );
 935
 936        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
 937
 938        cx.simulate_keystroke("b");
 939
 940        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
 941    }
 942
 943    #[gpui::test]
 944    async fn test_delete(cx: &mut gpui::TestAppContext) {
 945        let mut cx = VimTestContext::new(cx, true).await;
 946        cx.enable_helix();
 947
 948        // test delete a selection
 949        cx.set_state(
 950            indoc! {"
 951            The qu«ick ˇ»brown
 952            fox jumps over
 953            the lazy dog."},
 954            Mode::HelixNormal,
 955        );
 956
 957        cx.simulate_keystrokes("d");
 958
 959        cx.assert_state(
 960            indoc! {"
 961            The quˇbrown
 962            fox jumps over
 963            the lazy dog."},
 964            Mode::HelixNormal,
 965        );
 966
 967        // test deleting a single character
 968        cx.simulate_keystrokes("d");
 969
 970        cx.assert_state(
 971            indoc! {"
 972            The quˇrown
 973            fox jumps over
 974            the lazy dog."},
 975            Mode::HelixNormal,
 976        );
 977    }
 978
 979    #[gpui::test]
 980    async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
 981        let mut cx = VimTestContext::new(cx, true).await;
 982
 983        cx.set_state(
 984            indoc! {"
 985            The quick brownˇ
 986            fox jumps over
 987            the lazy dog."},
 988            Mode::HelixNormal,
 989        );
 990
 991        cx.simulate_keystrokes("d");
 992
 993        cx.assert_state(
 994            indoc! {"
 995            The quick brownˇfox jumps over
 996            the lazy dog."},
 997            Mode::HelixNormal,
 998        );
 999    }
1000
1001    // #[gpui::test]
1002    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1003    //     let mut cx = VimTestContext::new(cx, true).await;
1004
1005    //     cx.set_state(
1006    //         indoc! {"
1007    //         The quick brown
1008    //         fox jumps over
1009    //         the lazy dog.ˇ"},
1010    //         Mode::HelixNormal,
1011    //     );
1012
1013    //     cx.simulate_keystrokes("d");
1014
1015    //     cx.assert_state(
1016    //         indoc! {"
1017    //         The quick brown
1018    //         fox jumps over
1019    //         the lazy dog.ˇ"},
1020    //         Mode::HelixNormal,
1021    //     );
1022    // }
1023
1024    #[gpui::test]
1025    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1026        let mut cx = VimTestContext::new(cx, true).await;
1027        cx.enable_helix();
1028
1029        cx.set_state(
1030            indoc! {"
1031            The quˇick brown
1032            fox jumps over
1033            the lazy dog."},
1034            Mode::HelixNormal,
1035        );
1036
1037        cx.simulate_keystrokes("f z");
1038
1039        cx.assert_state(
1040            indoc! {"
1041                The qu«ick brown
1042                fox jumps over
1043                the lazˇ»y dog."},
1044            Mode::HelixNormal,
1045        );
1046
1047        cx.simulate_keystrokes("F e F e");
1048
1049        cx.assert_state(
1050            indoc! {"
1051                The quick brown
1052                fox jumps ov«ˇer
1053                the» lazy dog."},
1054            Mode::HelixNormal,
1055        );
1056
1057        cx.simulate_keystrokes("e 2 F e");
1058
1059        cx.assert_state(
1060            indoc! {"
1061                Th«ˇe quick brown
1062                fox jumps over»
1063                the lazy dog."},
1064            Mode::HelixNormal,
1065        );
1066
1067        cx.simulate_keystrokes("t r t r");
1068
1069        cx.assert_state(
1070            indoc! {"
1071                The quick «brown
1072                fox jumps oveˇ»r
1073                the lazy dog."},
1074            Mode::HelixNormal,
1075        );
1076    }
1077
1078    #[gpui::test]
1079    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1080        let mut cx = VimTestContext::new(cx, true).await;
1081        cx.enable_helix();
1082
1083        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1084
1085        cx.simulate_keystroke("w");
1086
1087        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1088
1089        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1090
1091        cx.simulate_keystroke("b");
1092
1093        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1094    }
1095
1096    #[gpui::test]
1097    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1098        let mut cx = VimTestContext::new(cx, true).await;
1099        cx.enable_helix();
1100        cx.set_state(
1101            indoc! {"
1102            «The ˇ»quick brown
1103            fox jumps over
1104            the lazy dog."},
1105            Mode::HelixNormal,
1106        );
1107
1108        cx.simulate_keystrokes("i");
1109
1110        cx.assert_state(
1111            indoc! {"
1112            ˇThe quick brown
1113            fox jumps over
1114            the lazy dog."},
1115            Mode::Insert,
1116        );
1117    }
1118
1119    #[gpui::test]
1120    async fn test_append(cx: &mut gpui::TestAppContext) {
1121        let mut cx = VimTestContext::new(cx, true).await;
1122        cx.enable_helix();
1123        // test from the end of the selection
1124        cx.set_state(
1125            indoc! {"
1126            «Theˇ» quick brown
1127            fox jumps over
1128            the lazy dog."},
1129            Mode::HelixNormal,
1130        );
1131
1132        cx.simulate_keystrokes("a");
1133
1134        cx.assert_state(
1135            indoc! {"
1136            Theˇ quick brown
1137            fox jumps over
1138            the lazy dog."},
1139            Mode::Insert,
1140        );
1141
1142        // test from the beginning of the selection
1143        cx.set_state(
1144            indoc! {"
1145            «ˇThe» quick brown
1146            fox jumps over
1147            the lazy dog."},
1148            Mode::HelixNormal,
1149        );
1150
1151        cx.simulate_keystrokes("a");
1152
1153        cx.assert_state(
1154            indoc! {"
1155            Theˇ quick brown
1156            fox jumps over
1157            the lazy dog."},
1158            Mode::Insert,
1159        );
1160    }
1161
1162    #[gpui::test]
1163    async fn test_replace(cx: &mut gpui::TestAppContext) {
1164        let mut cx = VimTestContext::new(cx, true).await;
1165        cx.enable_helix();
1166
1167        // No selection (single character)
1168        cx.set_state("ˇaa", Mode::HelixNormal);
1169
1170        cx.simulate_keystrokes("r x");
1171
1172        cx.assert_state("ˇxa", Mode::HelixNormal);
1173
1174        // Cursor at the beginning
1175        cx.set_state("«ˇaa»", Mode::HelixNormal);
1176
1177        cx.simulate_keystrokes("r x");
1178
1179        cx.assert_state("«ˇxx»", Mode::HelixNormal);
1180
1181        // Cursor at the end
1182        cx.set_state("«aaˇ»", Mode::HelixNormal);
1183
1184        cx.simulate_keystrokes("r x");
1185
1186        cx.assert_state("«xxˇ»", Mode::HelixNormal);
1187    }
1188
1189    #[gpui::test]
1190    async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1191        let mut cx = VimTestContext::new(cx, true).await;
1192        cx.enable_helix();
1193
1194        // Test yanking current character with no selection
1195        cx.set_state("hello ˇworld", Mode::HelixNormal);
1196        cx.simulate_keystrokes("y");
1197
1198        // Test cursor remains at the same position after yanking single character
1199        cx.assert_state("hello ˇworld", Mode::HelixNormal);
1200        cx.shared_clipboard().assert_eq("w");
1201
1202        // Move cursor and yank another character
1203        cx.simulate_keystrokes("l");
1204        cx.simulate_keystrokes("y");
1205        cx.shared_clipboard().assert_eq("o");
1206
1207        // Test yanking with existing selection
1208        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1209        cx.simulate_keystrokes("y");
1210        cx.shared_clipboard().assert_eq("worl");
1211        cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1212
1213        // Test yanking in select mode character by character
1214        cx.set_state("hello ˇworld", Mode::HelixNormal);
1215        cx.simulate_keystroke("v");
1216        cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1217        cx.simulate_keystroke("y");
1218        cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1219        cx.shared_clipboard().assert_eq("w");
1220    }
1221
1222    #[gpui::test]
1223    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1224        let mut cx = VimTestContext::new(cx, true).await;
1225        cx.enable_helix();
1226
1227        // First copy some text to clipboard
1228        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1229        cx.simulate_keystrokes("y");
1230
1231        // Test paste with shift-r on single cursor
1232        cx.set_state("foo ˇbar", Mode::HelixNormal);
1233        cx.simulate_keystrokes("shift-r");
1234
1235        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1236
1237        // Test paste with shift-r on selection
1238        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1239        cx.simulate_keystrokes("shift-r");
1240
1241        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1242    }
1243
1244    #[gpui::test]
1245    async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1246        let mut cx = VimTestContext::new(cx, true).await;
1247
1248        assert_eq!(cx.mode(), Mode::Normal);
1249        cx.enable_helix();
1250
1251        cx.simulate_keystrokes("v");
1252        assert_eq!(cx.mode(), Mode::HelixSelect);
1253        cx.simulate_keystrokes("escape");
1254        assert_eq!(cx.mode(), Mode::HelixNormal);
1255    }
1256
1257    #[gpui::test]
1258    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1259        let mut cx = VimTestContext::new(cx, true).await;
1260        cx.enable_helix();
1261
1262        // Make a modification at a specific location
1263        cx.set_state("ˇhello", Mode::HelixNormal);
1264        assert_eq!(cx.mode(), Mode::HelixNormal);
1265        cx.simulate_keystrokes("i");
1266        assert_eq!(cx.mode(), Mode::Insert);
1267        cx.simulate_keystrokes("escape");
1268        assert_eq!(cx.mode(), Mode::HelixNormal);
1269    }
1270
1271    #[gpui::test]
1272    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1273        let mut cx = VimTestContext::new(cx, true).await;
1274        cx.enable_helix();
1275
1276        // Make a modification at a specific location
1277        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1278        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1279        cx.simulate_keystrokes("i");
1280        cx.simulate_keystrokes("escape");
1281        cx.simulate_keystrokes("i");
1282        cx.simulate_keystrokes("m o d i f i e d space");
1283        cx.simulate_keystrokes("escape");
1284
1285        // TODO: this fails, because state is no longer helix
1286        cx.assert_state(
1287            "line one\nline modified ˇtwo\nline three",
1288            Mode::HelixNormal,
1289        );
1290
1291        // Move cursor away from the modification
1292        cx.simulate_keystrokes("up");
1293
1294        // Use "g ." to go back to last modification
1295        cx.simulate_keystrokes("g .");
1296
1297        // Verify we're back at the modification location and still in HelixNormal mode
1298        cx.assert_state(
1299            "line one\nline modifiedˇ two\nline three",
1300            Mode::HelixNormal,
1301        );
1302    }
1303
1304    #[gpui::test]
1305    async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1306        let mut cx = VimTestContext::new(cx, true).await;
1307        cx.set_state(
1308            "line one\nline ˇtwo\nline three\nline four",
1309            Mode::HelixNormal,
1310        );
1311        cx.simulate_keystrokes("2 x");
1312        cx.assert_state(
1313            "line one\n«line two\nline three\nˇ»line four",
1314            Mode::HelixNormal,
1315        );
1316
1317        // Test extending existing line selection
1318        cx.set_state(
1319            indoc! {"
1320            li«ˇne one
1321            li»ne two
1322            line three
1323            line four"},
1324            Mode::HelixNormal,
1325        );
1326        cx.simulate_keystrokes("x");
1327        cx.assert_state(
1328            indoc! {"
1329            «line one
1330            line two
1331            ˇ»line three
1332            line four"},
1333            Mode::HelixNormal,
1334        );
1335
1336        // Pressing x in empty line, select next line (because helix considers cursor a selection)
1337        cx.set_state(
1338            indoc! {"
1339            line one
1340            ˇ
1341            line three
1342            line four
1343            line five
1344            line six"},
1345            Mode::HelixNormal,
1346        );
1347        cx.simulate_keystrokes("x");
1348        cx.assert_state(
1349            indoc! {"
1350            line one
1351            «
1352            line three
1353            ˇ»line four
1354            line five
1355            line six"},
1356            Mode::HelixNormal,
1357        );
1358
1359        // Another x should only select the next line
1360        cx.simulate_keystrokes("x");
1361        cx.assert_state(
1362            indoc! {"
1363            line one
1364            «
1365            line three
1366            line four
1367            ˇ»line five
1368            line six"},
1369            Mode::HelixNormal,
1370        );
1371
1372        // Empty line with count selects extra + count lines
1373        cx.set_state(
1374            indoc! {"
1375            line one
1376            ˇ
1377            line three
1378            line four
1379            line five"},
1380            Mode::HelixNormal,
1381        );
1382        cx.simulate_keystrokes("2 x");
1383        cx.assert_state(
1384            indoc! {"
1385            line one
1386            «
1387            line three
1388            line four
1389            ˇ»line five"},
1390            Mode::HelixNormal,
1391        );
1392
1393        // Compare empty vs non-empty line behavior
1394        cx.set_state(
1395            indoc! {"
1396            ˇnon-empty line
1397            line two
1398            line three"},
1399            Mode::HelixNormal,
1400        );
1401        cx.simulate_keystrokes("x");
1402        cx.assert_state(
1403            indoc! {"
1404            «non-empty line
1405            ˇ»line two
1406            line three"},
1407            Mode::HelixNormal,
1408        );
1409
1410        // Same test but with empty line - should select one extra
1411        cx.set_state(
1412            indoc! {"
1413            ˇ
1414            line two
1415            line three"},
1416            Mode::HelixNormal,
1417        );
1418        cx.simulate_keystrokes("x");
1419        cx.assert_state(
1420            indoc! {"
1421            «
1422            line two
1423            ˇ»line three"},
1424            Mode::HelixNormal,
1425        );
1426
1427        // Test selecting multiple lines with count
1428        cx.set_state(
1429            indoc! {"
1430            ˇline one
1431            line two
1432            line threeˇ
1433            line four
1434            line five"},
1435            Mode::HelixNormal,
1436        );
1437        cx.simulate_keystrokes("x");
1438        cx.assert_state(
1439            indoc! {"
1440            «line one
1441            ˇ»line two
1442            «line three
1443            ˇ»line four
1444            line five"},
1445            Mode::HelixNormal,
1446        );
1447        cx.simulate_keystrokes("x");
1448        // Adjacent line selections stay separate (not merged)
1449        cx.assert_state(
1450            indoc! {"
1451            «line one
1452            line two
1453            ˇ»«line three
1454            line four
1455            ˇ»line five"},
1456            Mode::HelixNormal,
1457        );
1458    }
1459
1460    #[gpui::test]
1461    async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1462        let mut cx = VimTestContext::new(cx, true).await;
1463
1464        assert_eq!(cx.mode(), Mode::Normal);
1465        cx.enable_helix();
1466
1467        cx.set_state("ˇhello", Mode::HelixNormal);
1468        cx.simulate_keystrokes("l v l l");
1469        cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1470    }
1471
1472    #[gpui::test]
1473    async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
1474        let mut cx = VimTestContext::new(cx, true).await;
1475
1476        assert_eq!(cx.mode(), Mode::Normal);
1477        cx.enable_helix();
1478
1479        // Start with multiple cursors (no selections)
1480        cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
1481
1482        // Enter select mode and move right twice
1483        cx.simulate_keystrokes("v l l");
1484
1485        // Each cursor should independently create and extend its own selection
1486        cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
1487    }
1488
1489    #[gpui::test]
1490    async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
1491        let mut cx = VimTestContext::new(cx, true).await;
1492
1493        cx.set_state("ˇone two", Mode::Normal);
1494        cx.simulate_keystrokes("v w");
1495        cx.assert_state("«one tˇ»wo", Mode::Visual);
1496
1497        // In Vim, this selects "t". In helix selections stops just before "t"
1498
1499        cx.enable_helix();
1500        cx.set_state("ˇone two", Mode::HelixNormal);
1501        cx.simulate_keystrokes("v w");
1502        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1503    }
1504
1505    #[gpui::test]
1506    async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
1507        let mut cx = VimTestContext::new(cx, true).await;
1508
1509        cx.set_state("ˇone two", Mode::Normal);
1510        cx.simulate_keystrokes("v w");
1511        cx.assert_state("«one tˇ»wo", Mode::Visual);
1512        cx.simulate_keystrokes("escape");
1513        cx.assert_state("one ˇtwo", Mode::Normal);
1514
1515        cx.enable_helix();
1516        cx.set_state("ˇone two", Mode::HelixNormal);
1517        cx.simulate_keystrokes("v w");
1518        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1519        cx.simulate_keystrokes("escape");
1520        cx.assert_state("«one ˇ»two", Mode::HelixNormal);
1521    }
1522
1523    #[gpui::test]
1524    async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
1525        let mut cx = VimTestContext::new(cx, true).await;
1526        cx.enable_helix();
1527
1528        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1529        cx.simulate_keystrokes("w");
1530        cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
1531
1532        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1533        cx.simulate_keystrokes("e");
1534        cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
1535    }
1536
1537    #[gpui::test]
1538    async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
1539        let mut cx = VimTestContext::new(cx, true).await;
1540        cx.enable_helix();
1541
1542        cx.set_state("ˇone two three", Mode::HelixNormal);
1543        cx.simulate_keystrokes("l l v h h h");
1544        cx.assert_state("«ˇone» two three", Mode::HelixSelect);
1545    }
1546
1547    #[gpui::test]
1548    async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
1549        let mut cx = VimTestContext::new(cx, true).await;
1550        cx.enable_helix();
1551
1552        cx.set_state("ˇone two one", Mode::HelixNormal);
1553        cx.simulate_keystrokes("x");
1554        cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
1555        cx.simulate_keystrokes("s o n e");
1556        cx.run_until_parked();
1557        cx.simulate_keystrokes("enter");
1558        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1559
1560        cx.simulate_keystrokes("x");
1561        cx.simulate_keystrokes("s");
1562        cx.run_until_parked();
1563        cx.simulate_keystrokes("enter");
1564        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1565
1566        // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
1567        // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
1568        // cx.simulate_keystrokes("s o n e enter");
1569        // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
1570    }
1571
1572    #[gpui::test]
1573    async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
1574        let mut cx = VimTestContext::new(cx, true).await;
1575
1576        cx.set_state("ˇhello two one two one two one", Mode::Visual);
1577        cx.simulate_keystrokes("/ o n e");
1578        cx.simulate_keystrokes("enter");
1579        cx.simulate_keystrokes("n n");
1580        cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
1581
1582        cx.set_state("ˇhello two one two one two one", Mode::Normal);
1583        cx.simulate_keystrokes("/ o n e");
1584        cx.simulate_keystrokes("enter");
1585        cx.simulate_keystrokes("n n");
1586        cx.assert_state("hello two one two one two ˇone", Mode::Normal);
1587
1588        cx.set_state("ˇhello two one two one two one", Mode::Normal);
1589        cx.simulate_keystrokes("/ o n e");
1590        cx.simulate_keystrokes("enter");
1591        cx.simulate_keystrokes("n g n g n");
1592        cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
1593
1594        cx.enable_helix();
1595
1596        cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
1597        cx.simulate_keystrokes("/ o n e");
1598        cx.simulate_keystrokes("enter");
1599        cx.simulate_keystrokes("n n");
1600        cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
1601
1602        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
1603        cx.simulate_keystrokes("/ o n e");
1604        cx.simulate_keystrokes("enter");
1605        cx.simulate_keystrokes("n n");
1606        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
1607    }
1608
1609    #[gpui::test]
1610    async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1611        let mut cx = VimTestContext::new(cx, true).await;
1612
1613        cx.set_state("ˇone two", Mode::HelixNormal);
1614        cx.simulate_keystrokes("c");
1615        cx.assert_state("ˇne two", Mode::Insert);
1616
1617        cx.set_state("«oneˇ» two", Mode::HelixNormal);
1618        cx.simulate_keystrokes("c");
1619        cx.assert_state("ˇ two", Mode::Insert);
1620
1621        cx.set_state(
1622            indoc! {"
1623            oneˇ two
1624            three
1625            "},
1626            Mode::HelixNormal,
1627        );
1628        cx.simulate_keystrokes("x c");
1629        cx.assert_state(
1630            indoc! {"
1631            ˇ
1632            three
1633            "},
1634            Mode::Insert,
1635        );
1636
1637        cx.set_state(
1638            indoc! {"
1639            one twoˇ
1640            three
1641            "},
1642            Mode::HelixNormal,
1643        );
1644        cx.simulate_keystrokes("c");
1645        cx.assert_state(
1646            indoc! {"
1647            one twoˇthree
1648            "},
1649            Mode::Insert,
1650        );
1651
1652        // Helix doesn't set the cursor to the first non-blank one when
1653        // replacing lines: it uses language-dependent indent queries instead.
1654        cx.set_state(
1655            indoc! {"
1656            one two
1657            «    indented
1658            three not indentedˇ»
1659            "},
1660            Mode::HelixNormal,
1661        );
1662        cx.simulate_keystrokes("c");
1663        cx.set_state(
1664            indoc! {"
1665            one two
1666            ˇ
1667            "},
1668            Mode::Insert,
1669        );
1670    }
1671
1672    #[gpui::test]
1673    async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
1674        let mut cx = VimTestContext::new(cx, true).await;
1675        cx.enable_helix();
1676
1677        // Test g l moves to last character, not after it
1678        cx.set_state("hello ˇworld!", Mode::HelixNormal);
1679        cx.simulate_keystrokes("g l");
1680        cx.assert_state("hello worldˇ!", Mode::HelixNormal);
1681
1682        // Test with Chinese characters, test if work with UTF-8?
1683        cx.set_state("ˇ你好世界", Mode::HelixNormal);
1684        cx.simulate_keystrokes("g l");
1685        cx.assert_state("你好世ˇ界", Mode::HelixNormal);
1686
1687        // Test with end of line
1688        cx.set_state("endˇ", Mode::HelixNormal);
1689        cx.simulate_keystrokes("g l");
1690        cx.assert_state("enˇd", Mode::HelixNormal);
1691
1692        // Test with empty line
1693        cx.set_state(
1694            indoc! {"
1695                hello
1696                ˇ
1697                world"},
1698            Mode::HelixNormal,
1699        );
1700        cx.simulate_keystrokes("g l");
1701        cx.assert_state(
1702            indoc! {"
1703                hello
1704                ˇ
1705                world"},
1706            Mode::HelixNormal,
1707        );
1708
1709        // Test with multiple lines
1710        cx.set_state(
1711            indoc! {"
1712                ˇfirst line
1713                second line
1714                third line"},
1715            Mode::HelixNormal,
1716        );
1717        cx.simulate_keystrokes("g l");
1718        cx.assert_state(
1719            indoc! {"
1720                first linˇe
1721                second line
1722                third line"},
1723            Mode::HelixNormal,
1724        );
1725    }
1726
1727    #[gpui::test]
1728    async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
1729        VimTestContext::init(cx);
1730
1731        let fs = FakeFs::new(cx.background_executor.clone());
1732        fs.insert_tree(
1733            path!("/dir"),
1734            json!({
1735                "file_a.rs": "// File A.",
1736                "file_b.rs": "// File B.",
1737            }),
1738        )
1739        .await;
1740
1741        let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1742        let workspace =
1743            cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx));
1744
1745        cx.update(|cx| {
1746            VimTestContext::init_keybindings(true, cx);
1747            SettingsStore::update_global(cx, |store, cx| {
1748                store.update_user_settings(cx, |store| store.helix_mode = Some(true));
1749            })
1750        });
1751
1752        let cx = &mut VisualTestContext::from_window(*workspace, cx);
1753
1754        workspace
1755            .update(cx, |workspace, window, cx| {
1756                ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
1757            })
1758            .unwrap();
1759
1760        let search_view = workspace
1761            .update(cx, |workspace, _, cx| {
1762                workspace
1763                    .active_pane()
1764                    .read(cx)
1765                    .items()
1766                    .find_map(|item| item.downcast::<ProjectSearchView>())
1767                    .expect("Project search view should be active")
1768            })
1769            .unwrap();
1770
1771        project_search::perform_project_search(&search_view, "File A", cx);
1772
1773        search_view.update(cx, |search_view, cx| {
1774            let vim_mode = search_view
1775                .results_editor()
1776                .read(cx)
1777                .addon::<VimAddon>()
1778                .map(|addon| addon.entity.read(cx).mode);
1779
1780            assert_eq!(vim_mode, Some(Mode::HelixNormal));
1781        });
1782    }
1783
1784    #[gpui::test]
1785    async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
1786        let mut cx = VimTestContext::new(cx, true).await;
1787        cx.enable_helix();
1788
1789        // Start with a selection
1790        cx.set_state(
1791            indoc! {"
1792            «lineˇ» one
1793            line two
1794            line three
1795            line four
1796            line five"},
1797            Mode::HelixNormal,
1798        );
1799
1800        // Scroll down, selection should collapse
1801        cx.simulate_keystrokes("ctrl-d");
1802        cx.assert_state(
1803            indoc! {"
1804            line one
1805            line two
1806            line three
1807            line four
1808            line fiveˇ"},
1809            Mode::HelixNormal,
1810        );
1811
1812        // Make a new selection
1813        cx.simulate_keystroke("b");
1814        cx.assert_state(
1815            indoc! {"
1816            line one
1817            line two
1818            line three
1819            line four
1820            line «ˇfive»"},
1821            Mode::HelixNormal,
1822        );
1823
1824        // And scroll up, once again collapsing the selection.
1825        cx.simulate_keystroke("ctrl-u");
1826        cx.assert_state(
1827            indoc! {"
1828            line one
1829            line two
1830            line three
1831            line ˇfour
1832            line five"},
1833            Mode::HelixNormal,
1834        );
1835
1836        // Enter select mode
1837        cx.simulate_keystroke("v");
1838        cx.assert_state(
1839            indoc! {"
1840            line one
1841            line two
1842            line three
1843            line «fˇ»our
1844            line five"},
1845            Mode::HelixSelect,
1846        );
1847
1848        // And now the selection should be kept/expanded.
1849        cx.simulate_keystroke("ctrl-d");
1850        cx.assert_state(
1851            indoc! {"
1852            line one
1853            line two
1854            line three
1855            line «four
1856            line fiveˇ»"},
1857            Mode::HelixSelect,
1858        );
1859    }
1860}