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