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(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 (map, selections) = editor.selections.all_display(cx);
 482
 483                // Store selection info for positioning after edit
 484                let selection_info: Vec<_> = selections
 485                    .iter()
 486                    .map(|selection| {
 487                        let range = selection.range();
 488                        let start_offset = range.start.to_offset(&map, Bias::Left);
 489                        let end_offset = range.end.to_offset(&map, Bias::Left);
 490                        let was_empty = range.is_empty();
 491                        let was_reversed = selection.reversed;
 492                        (
 493                            map.buffer_snapshot().anchor_before(start_offset),
 494                            end_offset - start_offset,
 495                            was_empty,
 496                            was_reversed,
 497                        )
 498                    })
 499                    .collect();
 500
 501                let mut edits = Vec::new();
 502                for selection in &selections {
 503                    let mut range = selection.range();
 504
 505                    // For empty selections, extend to replace one character
 506                    if range.is_empty() {
 507                        range.end = movement::saturating_right(&map, range.start);
 508                    }
 509
 510                    let byte_range = range.start.to_offset(&map, Bias::Left)
 511                        ..range.end.to_offset(&map, Bias::Left);
 512
 513                    if !byte_range.is_empty() {
 514                        let replacement_text = text.repeat(byte_range.len());
 515                        edits.push((byte_range, replacement_text));
 516                    }
 517                }
 518
 519                editor.edit(edits, cx);
 520
 521                // Restore selections based on original info
 522                let snapshot = editor.buffer().read(cx).snapshot(cx);
 523                let ranges: Vec<_> = selection_info
 524                    .into_iter()
 525                    .map(|(start_anchor, original_len, was_empty, was_reversed)| {
 526                        let start_point = start_anchor.to_point(&snapshot);
 527                        if was_empty {
 528                            // For cursor-only, collapse to start
 529                            start_point..start_point
 530                        } else {
 531                            // For selections, span the replaced text
 532                            let replacement_len = text.len() * original_len;
 533                            let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
 534                            let end_point = snapshot.offset_to_point(end_offset);
 535                            if was_reversed {
 536                                end_point..start_point
 537                            } else {
 538                                start_point..end_point
 539                            }
 540                        }
 541                    })
 542                    .collect();
 543
 544                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 545                    s.select_ranges(ranges);
 546                });
 547            });
 548        });
 549        self.switch_mode(Mode::HelixNormal, true, window, cx);
 550    }
 551
 552    pub fn helix_goto_last_modification(
 553        &mut self,
 554        _: &HelixGotoLastModification,
 555        window: &mut Window,
 556        cx: &mut Context<Self>,
 557    ) {
 558        self.jump(".".into(), false, false, window, cx);
 559    }
 560
 561    pub fn helix_select_lines(
 562        &mut self,
 563        _: &HelixSelectLine,
 564        window: &mut Window,
 565        cx: &mut Context<Self>,
 566    ) {
 567        let count = Vim::take_count(cx).unwrap_or(1);
 568        self.update_editor(cx, |_, editor, cx| {
 569            editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 570            let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
 571            let mut selections = editor.selections.all::<Point>(cx);
 572            let max_point = display_map.buffer_snapshot().max_point();
 573            let buffer_snapshot = &display_map.buffer_snapshot();
 574
 575            for selection in &mut selections {
 576                // Start always goes to column 0 of the first selected line
 577                let start_row = selection.start.row;
 578                let current_end_row = selection.end.row;
 579
 580                // Check if cursor is on empty line by checking first character
 581                let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
 582                let first_char = buffer_snapshot.chars_at(line_start_offset).next();
 583                let extra_line = if first_char == Some('\n') { 1 } else { 0 };
 584
 585                let end_row = current_end_row + count as u32 + extra_line;
 586
 587                selection.start = Point::new(start_row, 0);
 588                selection.end = if end_row > max_point.row {
 589                    max_point
 590                } else {
 591                    Point::new(end_row, 0)
 592                };
 593                selection.reversed = false;
 594            }
 595
 596            editor.change_selections(Default::default(), window, cx, |s| {
 597                s.select(selections);
 598            });
 599        });
 600    }
 601
 602    fn helix_keep_newest_selection(
 603        &mut self,
 604        _: &HelixKeepNewestSelection,
 605        window: &mut Window,
 606        cx: &mut Context<Self>,
 607    ) {
 608        self.update_editor(cx, |_, editor, cx| {
 609            let newest = editor.selections.newest::<usize>(cx);
 610            editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
 611        });
 612    }
 613
 614    fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
 615        self.update_editor(cx, |vim, editor, cx| {
 616            editor.set_clip_at_line_ends(false, cx);
 617            editor.transact(window, cx, |editor, window, cx| {
 618                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 619                    s.move_with(|map, selection| {
 620                        if selection.start == selection.end {
 621                            selection.end = movement::right(map, selection.end);
 622                        }
 623
 624                        // If the selection starts and ends on a newline, we exclude the last one.
 625                        if !selection.is_empty()
 626                            && selection.start.column() == 0
 627                            && selection.end.column() == 0
 628                        {
 629                            selection.end = movement::left(map, selection.end);
 630                        }
 631                    })
 632                });
 633                if yank {
 634                    vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
 635                }
 636                let selections = editor.selections.all::<Point>(cx).into_iter();
 637                let edits = selections.map(|selection| (selection.start..selection.end, ""));
 638                editor.edit(edits, cx);
 639            });
 640        });
 641        self.switch_mode(Mode::Insert, true, window, cx);
 642    }
 643
 644    fn helix_substitute(
 645        &mut self,
 646        _: &HelixSubstitute,
 647        window: &mut Window,
 648        cx: &mut Context<Self>,
 649    ) {
 650        self.do_helix_substitute(true, window, cx);
 651    }
 652
 653    fn helix_substitute_no_yank(
 654        &mut self,
 655        _: &HelixSubstituteNoYank,
 656        window: &mut Window,
 657        cx: &mut Context<Self>,
 658    ) {
 659        self.do_helix_substitute(false, window, cx);
 660    }
 661}
 662
 663#[cfg(test)]
 664mod test {
 665    use indoc::indoc;
 666
 667    use crate::{state::Mode, test::VimTestContext};
 668
 669    #[gpui::test]
 670    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
 671        let mut cx = VimTestContext::new(cx, true).await;
 672        cx.enable_helix();
 673        // «
 674        // ˇ
 675        // »
 676        cx.set_state(
 677            indoc! {"
 678            Th«e quiˇ»ck brown
 679            fox jumps over
 680            the lazy dog."},
 681            Mode::HelixNormal,
 682        );
 683
 684        cx.simulate_keystrokes("w");
 685
 686        cx.assert_state(
 687            indoc! {"
 688            The qu«ick ˇ»brown
 689            fox jumps over
 690            the lazy dog."},
 691            Mode::HelixNormal,
 692        );
 693
 694        cx.simulate_keystrokes("w");
 695
 696        cx.assert_state(
 697            indoc! {"
 698            The quick «brownˇ»
 699            fox jumps over
 700            the lazy dog."},
 701            Mode::HelixNormal,
 702        );
 703
 704        cx.simulate_keystrokes("2 b");
 705
 706        cx.assert_state(
 707            indoc! {"
 708            The «ˇquick »brown
 709            fox jumps over
 710            the lazy dog."},
 711            Mode::HelixNormal,
 712        );
 713
 714        cx.simulate_keystrokes("down e up");
 715
 716        cx.assert_state(
 717            indoc! {"
 718            The quicˇk brown
 719            fox jumps over
 720            the lazy dog."},
 721            Mode::HelixNormal,
 722        );
 723
 724        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
 725
 726        cx.simulate_keystroke("b");
 727
 728        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
 729    }
 730
 731    #[gpui::test]
 732    async fn test_delete(cx: &mut gpui::TestAppContext) {
 733        let mut cx = VimTestContext::new(cx, true).await;
 734        cx.enable_helix();
 735
 736        // test delete a selection
 737        cx.set_state(
 738            indoc! {"
 739            The qu«ick ˇ»brown
 740            fox jumps over
 741            the lazy dog."},
 742            Mode::HelixNormal,
 743        );
 744
 745        cx.simulate_keystrokes("d");
 746
 747        cx.assert_state(
 748            indoc! {"
 749            The quˇbrown
 750            fox jumps over
 751            the lazy dog."},
 752            Mode::HelixNormal,
 753        );
 754
 755        // test deleting a single character
 756        cx.simulate_keystrokes("d");
 757
 758        cx.assert_state(
 759            indoc! {"
 760            The quˇrown
 761            fox jumps over
 762            the lazy dog."},
 763            Mode::HelixNormal,
 764        );
 765    }
 766
 767    #[gpui::test]
 768    async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
 769        let mut cx = VimTestContext::new(cx, true).await;
 770
 771        cx.set_state(
 772            indoc! {"
 773            The quick brownˇ
 774            fox jumps over
 775            the lazy dog."},
 776            Mode::HelixNormal,
 777        );
 778
 779        cx.simulate_keystrokes("d");
 780
 781        cx.assert_state(
 782            indoc! {"
 783            The quick brownˇfox jumps over
 784            the lazy dog."},
 785            Mode::HelixNormal,
 786        );
 787    }
 788
 789    // #[gpui::test]
 790    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
 791    //     let mut cx = VimTestContext::new(cx, true).await;
 792
 793    //     cx.set_state(
 794    //         indoc! {"
 795    //         The quick brown
 796    //         fox jumps over
 797    //         the lazy dog.ˇ"},
 798    //         Mode::HelixNormal,
 799    //     );
 800
 801    //     cx.simulate_keystrokes("d");
 802
 803    //     cx.assert_state(
 804    //         indoc! {"
 805    //         The quick brown
 806    //         fox jumps over
 807    //         the lazy dog.ˇ"},
 808    //         Mode::HelixNormal,
 809    //     );
 810    // }
 811
 812    #[gpui::test]
 813    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
 814        let mut cx = VimTestContext::new(cx, true).await;
 815        cx.enable_helix();
 816
 817        cx.set_state(
 818            indoc! {"
 819            The quˇick brown
 820            fox jumps over
 821            the lazy dog."},
 822            Mode::HelixNormal,
 823        );
 824
 825        cx.simulate_keystrokes("f z");
 826
 827        cx.assert_state(
 828            indoc! {"
 829                The qu«ick brown
 830                fox jumps over
 831                the lazˇ»y dog."},
 832            Mode::HelixNormal,
 833        );
 834
 835        cx.simulate_keystrokes("F e F e");
 836
 837        cx.assert_state(
 838            indoc! {"
 839                The quick brown
 840                fox jumps ov«ˇer
 841                the» lazy dog."},
 842            Mode::HelixNormal,
 843        );
 844
 845        cx.simulate_keystrokes("e 2 F e");
 846
 847        cx.assert_state(
 848            indoc! {"
 849                Th«ˇe quick brown
 850                fox jumps over»
 851                the lazy dog."},
 852            Mode::HelixNormal,
 853        );
 854
 855        cx.simulate_keystrokes("t r t r");
 856
 857        cx.assert_state(
 858            indoc! {"
 859                The quick «brown
 860                fox jumps oveˇ»r
 861                the lazy dog."},
 862            Mode::HelixNormal,
 863        );
 864    }
 865
 866    #[gpui::test]
 867    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
 868        let mut cx = VimTestContext::new(cx, true).await;
 869        cx.enable_helix();
 870
 871        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
 872
 873        cx.simulate_keystroke("w");
 874
 875        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
 876
 877        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
 878
 879        cx.simulate_keystroke("b");
 880
 881        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
 882    }
 883
 884    #[gpui::test]
 885    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
 886        let mut cx = VimTestContext::new(cx, true).await;
 887        cx.enable_helix();
 888        cx.set_state(
 889            indoc! {"
 890            «The ˇ»quick brown
 891            fox jumps over
 892            the lazy dog."},
 893            Mode::HelixNormal,
 894        );
 895
 896        cx.simulate_keystrokes("i");
 897
 898        cx.assert_state(
 899            indoc! {"
 900            ˇThe quick brown
 901            fox jumps over
 902            the lazy dog."},
 903            Mode::Insert,
 904        );
 905    }
 906
 907    #[gpui::test]
 908    async fn test_append(cx: &mut gpui::TestAppContext) {
 909        let mut cx = VimTestContext::new(cx, true).await;
 910        cx.enable_helix();
 911        // test from the end of the selection
 912        cx.set_state(
 913            indoc! {"
 914            «Theˇ» quick brown
 915            fox jumps over
 916            the lazy dog."},
 917            Mode::HelixNormal,
 918        );
 919
 920        cx.simulate_keystrokes("a");
 921
 922        cx.assert_state(
 923            indoc! {"
 924            Theˇ quick brown
 925            fox jumps over
 926            the lazy dog."},
 927            Mode::Insert,
 928        );
 929
 930        // test from the beginning of the selection
 931        cx.set_state(
 932            indoc! {"
 933            «ˇThe» quick brown
 934            fox jumps over
 935            the lazy dog."},
 936            Mode::HelixNormal,
 937        );
 938
 939        cx.simulate_keystrokes("a");
 940
 941        cx.assert_state(
 942            indoc! {"
 943            Theˇ quick brown
 944            fox jumps over
 945            the lazy dog."},
 946            Mode::Insert,
 947        );
 948    }
 949
 950    #[gpui::test]
 951    async fn test_replace(cx: &mut gpui::TestAppContext) {
 952        let mut cx = VimTestContext::new(cx, true).await;
 953        cx.enable_helix();
 954
 955        // No selection (single character)
 956        cx.set_state("ˇaa", Mode::HelixNormal);
 957
 958        cx.simulate_keystrokes("r x");
 959
 960        cx.assert_state("ˇxa", Mode::HelixNormal);
 961
 962        // Cursor at the beginning
 963        cx.set_state("«ˇaa»", Mode::HelixNormal);
 964
 965        cx.simulate_keystrokes("r x");
 966
 967        cx.assert_state("«ˇxx»", Mode::HelixNormal);
 968
 969        // Cursor at the end
 970        cx.set_state("«aaˇ»", Mode::HelixNormal);
 971
 972        cx.simulate_keystrokes("r x");
 973
 974        cx.assert_state("«xxˇ»", Mode::HelixNormal);
 975    }
 976
 977    #[gpui::test]
 978    async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
 979        let mut cx = VimTestContext::new(cx, true).await;
 980        cx.enable_helix();
 981
 982        // Test yanking current character with no selection
 983        cx.set_state("hello ˇworld", Mode::HelixNormal);
 984        cx.simulate_keystrokes("y");
 985
 986        // Test cursor remains at the same position after yanking single character
 987        cx.assert_state("hello ˇworld", Mode::HelixNormal);
 988        cx.shared_clipboard().assert_eq("w");
 989
 990        // Move cursor and yank another character
 991        cx.simulate_keystrokes("l");
 992        cx.simulate_keystrokes("y");
 993        cx.shared_clipboard().assert_eq("o");
 994
 995        // Test yanking with existing selection
 996        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
 997        cx.simulate_keystrokes("y");
 998        cx.shared_clipboard().assert_eq("worl");
 999        cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1000
1001        // Test yanking in select mode character by character
1002        cx.set_state("hello ˇworld", Mode::HelixNormal);
1003        cx.simulate_keystroke("v");
1004        cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1005        cx.simulate_keystroke("y");
1006        cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1007        cx.shared_clipboard().assert_eq("w");
1008    }
1009
1010    #[gpui::test]
1011    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1012        let mut cx = VimTestContext::new(cx, true).await;
1013        cx.enable_helix();
1014
1015        // First copy some text to clipboard
1016        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1017        cx.simulate_keystrokes("y");
1018
1019        // Test paste with shift-r on single cursor
1020        cx.set_state("foo ˇbar", Mode::HelixNormal);
1021        cx.simulate_keystrokes("shift-r");
1022
1023        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1024
1025        // Test paste with shift-r on selection
1026        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1027        cx.simulate_keystrokes("shift-r");
1028
1029        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1030    }
1031
1032    #[gpui::test]
1033    async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1034        let mut cx = VimTestContext::new(cx, true).await;
1035
1036        assert_eq!(cx.mode(), Mode::Normal);
1037        cx.enable_helix();
1038
1039        cx.simulate_keystrokes("v");
1040        assert_eq!(cx.mode(), Mode::HelixSelect);
1041        cx.simulate_keystrokes("escape");
1042        assert_eq!(cx.mode(), Mode::HelixNormal);
1043    }
1044
1045    #[gpui::test]
1046    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1047        let mut cx = VimTestContext::new(cx, true).await;
1048        cx.enable_helix();
1049
1050        // Make a modification at a specific location
1051        cx.set_state("ˇhello", Mode::HelixNormal);
1052        assert_eq!(cx.mode(), Mode::HelixNormal);
1053        cx.simulate_keystrokes("i");
1054        assert_eq!(cx.mode(), Mode::Insert);
1055        cx.simulate_keystrokes("escape");
1056        assert_eq!(cx.mode(), Mode::HelixNormal);
1057    }
1058
1059    #[gpui::test]
1060    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1061        let mut cx = VimTestContext::new(cx, true).await;
1062        cx.enable_helix();
1063
1064        // Make a modification at a specific location
1065        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1066        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1067        cx.simulate_keystrokes("i");
1068        cx.simulate_keystrokes("escape");
1069        cx.simulate_keystrokes("i");
1070        cx.simulate_keystrokes("m o d i f i e d space");
1071        cx.simulate_keystrokes("escape");
1072
1073        // TODO: this fails, because state is no longer helix
1074        cx.assert_state(
1075            "line one\nline modified ˇtwo\nline three",
1076            Mode::HelixNormal,
1077        );
1078
1079        // Move cursor away from the modification
1080        cx.simulate_keystrokes("up");
1081
1082        // Use "g ." to go back to last modification
1083        cx.simulate_keystrokes("g .");
1084
1085        // Verify we're back at the modification location and still in HelixNormal mode
1086        cx.assert_state(
1087            "line one\nline modifiedˇ two\nline three",
1088            Mode::HelixNormal,
1089        );
1090    }
1091
1092    #[gpui::test]
1093    async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1094        let mut cx = VimTestContext::new(cx, true).await;
1095        cx.set_state(
1096            "line one\nline ˇtwo\nline three\nline four",
1097            Mode::HelixNormal,
1098        );
1099        cx.simulate_keystrokes("2 x");
1100        cx.assert_state(
1101            "line one\n«line two\nline three\nˇ»line four",
1102            Mode::HelixNormal,
1103        );
1104
1105        // Test extending existing line selection
1106        cx.set_state(
1107            indoc! {"
1108            li«ˇne one
1109            li»ne two
1110            line three
1111            line four"},
1112            Mode::HelixNormal,
1113        );
1114        cx.simulate_keystrokes("x");
1115        cx.assert_state(
1116            indoc! {"
1117            «line one
1118            line two
1119            ˇ»line three
1120            line four"},
1121            Mode::HelixNormal,
1122        );
1123
1124        // Pressing x in empty line, select next line (because helix considers cursor a selection)
1125        cx.set_state(
1126            indoc! {"
1127            line one
1128            ˇ
1129            line three
1130            line four"},
1131            Mode::HelixNormal,
1132        );
1133        cx.simulate_keystrokes("x");
1134        cx.assert_state(
1135            indoc! {"
1136            line one
1137            «
1138            line three
1139            ˇ»line four"},
1140            Mode::HelixNormal,
1141        );
1142
1143        // Empty line with count selects extra + count lines
1144        cx.set_state(
1145            indoc! {"
1146            line one
1147            ˇ
1148            line three
1149            line four
1150            line five"},
1151            Mode::HelixNormal,
1152        );
1153        cx.simulate_keystrokes("2 x");
1154        cx.assert_state(
1155            indoc! {"
1156            line one
1157            «
1158            line three
1159            line four
1160            ˇ»line five"},
1161            Mode::HelixNormal,
1162        );
1163
1164        // Compare empty vs non-empty line behavior
1165        cx.set_state(
1166            indoc! {"
1167            ˇnon-empty line
1168            line two
1169            line three"},
1170            Mode::HelixNormal,
1171        );
1172        cx.simulate_keystrokes("x");
1173        cx.assert_state(
1174            indoc! {"
1175            «non-empty line
1176            ˇ»line two
1177            line three"},
1178            Mode::HelixNormal,
1179        );
1180
1181        // Same test but with empty line - should select one extra
1182        cx.set_state(
1183            indoc! {"
1184            ˇ
1185            line two
1186            line three"},
1187            Mode::HelixNormal,
1188        );
1189        cx.simulate_keystrokes("x");
1190        cx.assert_state(
1191            indoc! {"
1192            «
1193            line two
1194            ˇ»line three"},
1195            Mode::HelixNormal,
1196        );
1197
1198        // Test selecting multiple lines with count
1199        cx.set_state(
1200            indoc! {"
1201            ˇline one
1202            line two
1203            line threeˇ
1204            line four
1205            line five"},
1206            Mode::HelixNormal,
1207        );
1208        cx.simulate_keystrokes("x");
1209        cx.assert_state(
1210            indoc! {"
1211            «line one
1212            ˇ»line two
1213            «line three
1214            ˇ»line four
1215            line five"},
1216            Mode::HelixNormal,
1217        );
1218        cx.simulate_keystrokes("x");
1219        cx.assert_state(
1220            indoc! {"
1221            «line one
1222            line two
1223            line three
1224            line four
1225            ˇ»line five"},
1226            Mode::HelixNormal,
1227        );
1228    }
1229
1230    #[gpui::test]
1231    async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1232        let mut cx = VimTestContext::new(cx, true).await;
1233
1234        assert_eq!(cx.mode(), Mode::Normal);
1235        cx.enable_helix();
1236
1237        cx.set_state("ˇhello", Mode::HelixNormal);
1238        cx.simulate_keystrokes("l v l l");
1239        cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1240    }
1241
1242    #[gpui::test]
1243    async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
1244        let mut cx = VimTestContext::new(cx, true).await;
1245
1246        assert_eq!(cx.mode(), Mode::Normal);
1247        cx.enable_helix();
1248
1249        // Start with multiple cursors (no selections)
1250        cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
1251
1252        // Enter select mode and move right twice
1253        cx.simulate_keystrokes("v l l");
1254
1255        // Each cursor should independently create and extend its own selection
1256        cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
1257    }
1258
1259    #[gpui::test]
1260    async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
1261        let mut cx = VimTestContext::new(cx, true).await;
1262
1263        cx.set_state("ˇone two", Mode::Normal);
1264        cx.simulate_keystrokes("v w");
1265        cx.assert_state("«one tˇ»wo", Mode::Visual);
1266
1267        // In Vim, this selects "t". In helix selections stops just before "t"
1268
1269        cx.enable_helix();
1270        cx.set_state("ˇone two", Mode::HelixNormal);
1271        cx.simulate_keystrokes("v w");
1272        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1273    }
1274
1275    #[gpui::test]
1276    async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
1277        let mut cx = VimTestContext::new(cx, true).await;
1278        cx.enable_helix();
1279
1280        cx.set_state("ˇone two one", Mode::HelixNormal);
1281        cx.simulate_keystrokes("x");
1282        cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
1283        cx.simulate_keystrokes("s o n e");
1284        cx.run_until_parked();
1285        cx.simulate_keystrokes("enter");
1286        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1287
1288        cx.simulate_keystrokes("x");
1289        cx.simulate_keystrokes("s");
1290        cx.run_until_parked();
1291        cx.simulate_keystrokes("enter");
1292        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1293
1294        cx.set_state("ˇone two one", Mode::HelixNormal);
1295        cx.simulate_keystrokes("s o n e enter");
1296        cx.assert_state("ˇone two one", Mode::HelixNormal);
1297    }
1298
1299    #[gpui::test]
1300    async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1301        let mut cx = VimTestContext::new(cx, true).await;
1302
1303        cx.set_state("ˇone two", Mode::HelixNormal);
1304        cx.simulate_keystrokes("c");
1305        cx.assert_state("ˇne two", Mode::Insert);
1306
1307        cx.set_state("«oneˇ» two", Mode::HelixNormal);
1308        cx.simulate_keystrokes("c");
1309        cx.assert_state("ˇ two", Mode::Insert);
1310
1311        cx.set_state(
1312            indoc! {"
1313            oneˇ two
1314            three
1315            "},
1316            Mode::HelixNormal,
1317        );
1318        cx.simulate_keystrokes("x c");
1319        cx.assert_state(
1320            indoc! {"
1321            ˇ
1322            three
1323            "},
1324            Mode::Insert,
1325        );
1326
1327        cx.set_state(
1328            indoc! {"
1329            one twoˇ
1330            three
1331            "},
1332            Mode::HelixNormal,
1333        );
1334        cx.simulate_keystrokes("c");
1335        cx.assert_state(
1336            indoc! {"
1337            one twoˇthree
1338            "},
1339            Mode::Insert,
1340        );
1341
1342        // Helix doesn't set the cursor to the first non-blank one when
1343        // replacing lines: it uses language-dependent indent queries instead.
1344        cx.set_state(
1345            indoc! {"
1346            one two
1347            «    indented
1348            three not indentedˇ»
1349            "},
1350            Mode::HelixNormal,
1351        );
1352        cx.simulate_keystrokes("c");
1353        cx.set_state(
1354            indoc! {"
1355            one two
1356            ˇ
1357            "},
1358            Mode::Insert,
1359        );
1360    }
1361}