helix.rs

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