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