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