helix.rs

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