helix.rs

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