Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:36:22.106Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

helix.rs

4398 lines · 155.3 KB · rust
1mod boundary;
2mod duplicate;
3mod object;
4mod paste;
5mod select;
6mod surround;
7
8use editor::display_map::{DisplayRow, DisplaySnapshot};
9use editor::{
10    DisplayPoint, Editor, EditorSettings, MultiBufferOffset, NavigationOverlayLabel,
11    NavigationTargetOverlay, SelectionEffects, ToOffset, ToPoint, movement,
12};
13use gpui::actions;
14use gpui::{App, Context, Font, Hsla, Pixels, TaskExt, Window, WindowTextSystem};
15use language::{CharClassifier, CharKind, Point, Selection};
16use multi_buffer::MultiBufferSnapshot;
17use search::{BufferSearchBar, SearchOptions};
18use settings::Settings;
19use text::{Bias, LineEnding, SelectionGoal};
20use theme::ActiveTheme as _;
21use ui::px;
22use workspace::searchable::{self, Direction, FilteredSearchRange};
23
24use crate::motion::{self, MotionKind};
25use crate::state::{HelixJumpBehaviour, HelixJumpLabel, Mode, Operator, SearchState};
26use crate::{
27    PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim,
28    motion::{Motion, right},
29};
30use std::ops::Range;
31
32actions!(
33    vim,
34    [
35        /// Yanks the current selection or character if no selection.
36        HelixYank,
37        /// Inserts at the beginning of the selection.
38        HelixInsert,
39        /// Appends at the end of the selection.
40        HelixAppend,
41        /// Inserts at the end of the current Helix cursor line.
42        HelixInsertEndOfLine,
43        /// Goes to the location of the last modification.
44        HelixGotoLastModification,
45        /// Select entire line or multiple lines, extending downwards.
46        HelixSelectLine,
47        /// Select all matches of a given pattern within the current selection.
48        HelixSelectRegex,
49        /// Removes all but the one selection that was created last.
50        /// `Newest` can eventually be `Primary`.
51        HelixKeepNewestSelection,
52        /// Copies all selections below.
53        HelixDuplicateBelow,
54        /// Copies all selections above.
55        HelixDuplicateAbove,
56        /// Delete the selection and enter edit mode.
57        HelixSubstitute,
58        /// Delete the selection and enter edit mode, without yanking the selection.
59        HelixSubstituteNoYank,
60        /// Activate Helix-style word jump labels.
61        HelixJumpToWord,
62        /// Select the next match for the current search query.
63        HelixSelectNext,
64        /// Select the previous match for the current search query.
65        HelixSelectPrevious,
66    ]
67);
68
69pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
70    Vim::action(editor, cx, Vim::helix_select_lines);
71    Vim::action(editor, cx, Vim::helix_insert);
72    Vim::action(editor, cx, Vim::helix_append);
73    Vim::action(editor, cx, Vim::helix_insert_end_of_line);
74    Vim::action(editor, cx, Vim::helix_yank);
75    Vim::action(editor, cx, Vim::helix_goto_last_modification);
76    Vim::action(editor, cx, Vim::helix_paste);
77    Vim::action(editor, cx, Vim::helix_select_regex);
78    Vim::action(editor, cx, Vim::helix_keep_newest_selection);
79    Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
80        let times = Vim::take_count(cx);
81        vim.helix_duplicate_selections_below(times, window, cx);
82    });
83    Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
84        let times = Vim::take_count(cx);
85        vim.helix_duplicate_selections_above(times, window, cx);
86    });
87    Vim::action(editor, cx, Vim::helix_substitute);
88    Vim::action(editor, cx, Vim::helix_substitute_no_yank);
89    Vim::action(editor, cx, Vim::helix_jump_to_word);
90    Vim::action(editor, cx, Vim::helix_select_next);
91    Vim::action(editor, cx, Vim::helix_select_previous);
92    Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
93        vim.clear_operator(window, cx);
94        vim.push_operator(Operator::HelixSurroundAdd, window, cx);
95    });
96    Vim::action(
97        editor,
98        cx,
99        |vim, _: &PushHelixSurroundReplace, window, cx| {
100            vim.clear_operator(window, cx);
101            vim.push_operator(
102                Operator::HelixSurroundReplace {
103                    replaced_char: None,
104                },
105                window,
106                cx,
107            );
108        },
109    );
110    Vim::action(
111        editor,
112        cx,
113        |vim, _: &PushHelixSurroundDelete, window, cx| {
114            vim.clear_operator(window, cx);
115            vim.push_operator(Operator::HelixSurroundDelete, window, cx);
116        },
117    );
118}
119
120/// Returns column 0 of the document's first line, imitating `gg` in Helix.
121///
122/// With a count, Helix treats it as a (1-based) line number, so a count of `n`
123/// targets buffer row `n - 1`, clamped to the last line.
124fn start_of_document(map: &DisplaySnapshot, times: Option<usize>) -> DisplayPoint {
125    let buffer_row = match times {
126        None => 0,
127        Some(times) => (times.saturating_sub(1) as u32).min(map.max_row().0),
128    };
129    map.point_to_display_point(Point::new(buffer_row, 0), Bias::Left)
130}
131
132/// Returns column 0 of the document's last line, imitating `ge` in Helix.
133///
134/// Helix ignores any count for `ge`, so it is not taken here.
135fn end_of_document(map: &DisplaySnapshot) -> DisplayPoint {
136    map.point_to_display_point(Point::new(map.max_row().0, 0), Bias::Left)
137}
138
139impl Vim {
140    pub fn helix_normal_motion(
141        &mut self,
142        motion: Motion,
143        times: Option<usize>,
144        window: &mut Window,
145        cx: &mut Context<Self>,
146    ) {
147        self.helix_move_cursor(motion, times, window, cx);
148    }
149
150    pub fn helix_select_motion(
151        &mut self,
152        motion: Motion,
153        times: Option<usize>,
154        window: &mut Window,
155        cx: &mut Context<Self>,
156    ) {
157        self.update_editor(cx, |_, editor, cx| {
158            let text_layout_details = editor.text_layout_details(window, cx);
159            editor.change_selections(Default::default(), window, cx, |s| {
160                if let Motion::ZedSearchResult { new_selections, .. } = &motion {
161                    s.select_anchor_ranges(new_selections.clone());
162                    return;
163                };
164
165                s.move_with(&mut |map, selection| {
166                    let was_reversed = selection.reversed;
167                    let mut current_head = selection.head();
168
169                    // our motions assume the current character is after the cursor,
170                    // but in (forward) visual mode the current character is just
171                    // before the end of the selection.
172                    if !selection.reversed && !selection.is_empty() {
173                        current_head = movement::left(map, selection.end)
174                    }
175
176                    let (new_head, goal) = match motion {
177                        Motion::StartOfDocument => {
178                            (start_of_document(map, times), SelectionGoal::None)
179                        }
180                        Motion::EndOfDocument => (end_of_document(map), SelectionGoal::None),
181                        // EndOfLine positions after the last character, but in
182                        // helix visual mode we want the selection to end ON the
183                        // last character. Adjust left here so the subsequent
184                        // right-expansion (below) includes the last char without
185                        // spilling into the newline.
186                        Motion::EndOfLine { .. } => {
187                            let (point, goal) = motion
188                                .move_point(
189                                    map,
190                                    current_head,
191                                    selection.goal,
192                                    times,
193                                    &text_layout_details,
194                                )
195                                .unwrap_or((current_head, selection.goal));
196                            (movement::saturating_left(map, point), goal)
197                        }
198                        // Going to next word start is special cased
199                        // since Vim differs from Helix in that motion
200                        // Vim: `w` goes to the first character of a word
201                        // Helix: `w` goes to the character before a word
202                        Motion::NextWordStart { ignore_punctuation } => {
203                            let mut head = movement::right(map, current_head);
204                            let classifier =
205                                map.buffer_snapshot().char_classifier_at(head.to_point(map));
206                            for _ in 0..times.unwrap_or(1) {
207                                let (_, new_head) =
208                                    movement::find_boundary_trail(map, head, &mut |left, right| {
209                                        Self::is_boundary_right(ignore_punctuation)(
210                                            left,
211                                            right,
212                                            &classifier,
213                                        )
214                                    });
215                                head = new_head;
216                            }
217                            head = movement::left(map, head);
218                            (head, SelectionGoal::None)
219                        }
220                        _ => motion
221                            .move_point(
222                                map,
223                                current_head,
224                                selection.goal,
225                                times,
226                                &text_layout_details,
227                            )
228                            .unwrap_or((current_head, selection.goal)),
229                    };
230
231                    selection.set_head(new_head, goal);
232
233                    // ensure the current character is included in the selection.
234                    if !selection.reversed {
235                        let next_point = movement::right(map, selection.end);
236
237                        selection.end = next_point;
238                    }
239
240                    // vim always ensures the anchor character stays selected.
241                    // if our selection has reversed, we need to move the opposite end
242                    // to ensure the anchor is still selected.
243                    if was_reversed && !selection.reversed {
244                        selection.start = movement::left(map, selection.start);
245                    } else if !was_reversed && selection.reversed {
246                        selection.end = movement::right(map, selection.end);
247                    }
248                })
249            });
250        });
251    }
252
253    /// Updates all selections based on where the cursors are.
254    fn helix_new_selections(
255        &mut self,
256        window: &mut Window,
257        cx: &mut Context<Self>,
258        change: &mut dyn FnMut(
259            // the start of the cursor
260            DisplayPoint,
261            &DisplaySnapshot,
262        ) -> Option<(DisplayPoint, DisplayPoint)>,
263    ) {
264        self.update_editor(cx, |_, editor, cx| {
265            editor.change_selections(Default::default(), window, cx, |s| {
266                s.move_with(&mut |map, selection| {
267                    let cursor_start = if selection.reversed || selection.is_empty() {
268                        selection.head()
269                    } else {
270                        movement::left(map, selection.head())
271                    };
272                    let Some((head, tail)) = change(cursor_start, map) else {
273                        return;
274                    };
275
276                    selection.set_head_tail(head, tail, SelectionGoal::None);
277                });
278            });
279        });
280    }
281
282    fn helix_find_range_forward(
283        &mut self,
284        times: Option<usize>,
285        window: &mut Window,
286        cx: &mut Context<Self>,
287        is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
288    ) {
289        let times = times.unwrap_or(1);
290        self.helix_new_selections(window, cx, &mut |cursor, map| {
291            let mut head = movement::right(map, cursor);
292            let mut tail = cursor;
293            let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
294            if head == map.max_point() {
295                return None;
296            }
297            for _ in 0..times {
298                let (maybe_next_tail, next_head) =
299                    movement::find_boundary_trail(map, head, &mut |left, right| {
300                        is_boundary(left, right, &classifier)
301                    });
302
303                if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
304                    break;
305                }
306
307                head = next_head;
308                if let Some(next_tail) = maybe_next_tail {
309                    tail = next_tail;
310                }
311            }
312            Some((head, tail))
313        });
314    }
315
316    fn helix_find_range_backward(
317        &mut self,
318        times: Option<usize>,
319        window: &mut Window,
320        cx: &mut Context<Self>,
321        is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
322    ) {
323        let times = times.unwrap_or(1);
324        self.helix_new_selections(window, cx, &mut |cursor, map| {
325            let mut head = cursor;
326            // The original cursor was one character wide,
327            // but the search starts from the left side of it,
328            // so to include that space the selection must end one character to the right.
329            let mut tail = movement::right(map, cursor);
330            let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
331            if head == DisplayPoint::zero() {
332                return None;
333            }
334            for _ in 0..times {
335                let (maybe_next_tail, next_head) =
336                    movement::find_preceding_boundary_trail(map, head, &mut |left, right| {
337                        is_boundary(left, right, &classifier)
338                    });
339
340                if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
341                    break;
342                }
343
344                head = next_head;
345                if let Some(next_tail) = maybe_next_tail {
346                    tail = next_tail;
347                }
348            }
349            Some((head, tail))
350        });
351    }
352
353    pub fn helix_move_and_collapse(
354        &mut self,
355        motion: Motion,
356        times: Option<usize>,
357        window: &mut Window,
358        cx: &mut Context<Self>,
359    ) {
360        self.update_editor(cx, |_, editor, cx| {
361            let text_layout_details = editor.text_layout_details(window, cx);
362            editor.change_selections(Default::default(), window, cx, |s| {
363                s.move_with(&mut |map, selection| {
364                    let goal = selection.goal;
365                    let cursor = if selection.is_empty() || selection.reversed {
366                        selection.head()
367                    } else {
368                        movement::left(map, selection.head())
369                    };
370
371                    let (point, goal) = motion
372                        .move_point(map, cursor, selection.goal, times, &text_layout_details)
373                        .unwrap_or((cursor, goal));
374
375                    selection.collapse_to(point, goal)
376                })
377            });
378        });
379    }
380
381    fn is_boundary_right(
382        ignore_punctuation: bool,
383    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
384        move |left, right, classifier| {
385            let left_kind = classifier.kind_with(left, ignore_punctuation);
386            let right_kind = classifier.kind_with(right, ignore_punctuation);
387            let at_newline = (left == '\n') ^ (right == '\n');
388
389            (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
390        }
391    }
392
393    fn is_boundary_left(
394        ignore_punctuation: bool,
395    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
396        move |left, right, classifier| {
397            let left_kind = classifier.kind_with(left, ignore_punctuation);
398            let right_kind = classifier.kind_with(right, ignore_punctuation);
399            let at_newline = (left == '\n') ^ (right == '\n');
400
401            (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
402        }
403    }
404
405    /// When `reversed` is true (used with `helix_find_range_backward`), the
406    /// `left` and `right` characters are yielded in reverse text order, so the
407    /// camelCase transition check must be flipped accordingly.
408    fn subword_boundary_start(
409        ignore_punctuation: bool,
410        reversed: bool,
411    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
412        move |left, right, classifier| {
413            let left_kind = classifier.kind_with(left, ignore_punctuation);
414            let right_kind = classifier.kind_with(right, ignore_punctuation);
415            let at_newline = (left == '\n') ^ (right == '\n');
416            let is_separator = |c: char| "_$=".contains(c);
417
418            let is_word = left_kind != right_kind && right_kind != CharKind::Whitespace;
419            let is_subword = (is_separator(left) && !is_separator(right))
420                || if reversed {
421                    right.is_lowercase() && left.is_uppercase()
422                } else {
423                    left.is_lowercase() && right.is_uppercase()
424                };
425
426            is_word || (is_subword && !right.is_whitespace()) || at_newline
427        }
428    }
429
430    /// When `reversed` is true (used with `helix_find_range_backward`), the
431    /// `left` and `right` characters are yielded in reverse text order, so the
432    /// camelCase transition check must be flipped accordingly.
433    fn subword_boundary_end(
434        ignore_punctuation: bool,
435        reversed: bool,
436    ) -> impl FnMut(char, char, &CharClassifier) -> bool {
437        move |left, right, classifier| {
438            let left_kind = classifier.kind_with(left, ignore_punctuation);
439            let right_kind = classifier.kind_with(right, ignore_punctuation);
440            let at_newline = (left == '\n') ^ (right == '\n');
441            let is_separator = |c: char| "_$=".contains(c);
442
443            let is_word = left_kind != right_kind && left_kind != CharKind::Whitespace;
444            let is_subword = (!is_separator(left) && is_separator(right))
445                || if reversed {
446                    right.is_lowercase() && left.is_uppercase()
447                } else {
448                    left.is_lowercase() && right.is_uppercase()
449                };
450
451            is_word || (is_subword && !left.is_whitespace()) || at_newline
452        }
453    }
454
455    pub fn helix_move_cursor(
456        &mut self,
457        motion: Motion,
458        times: Option<usize>,
459        window: &mut Window,
460        cx: &mut Context<Self>,
461    ) {
462        match motion {
463            Motion::NextWordStart { ignore_punctuation } => {
464                let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
465                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
466            }
467            Motion::NextWordEnd { ignore_punctuation } => {
468                let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
469                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
470            }
471            Motion::PreviousWordStart { ignore_punctuation } => {
472                let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
473                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
474            }
475            Motion::PreviousWordEnd { ignore_punctuation } => {
476                let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
477                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
478            }
479            // The subword motions implementation is based off of the same
480            // commands present in Helix itself, namely:
481            //
482            // * `move_next_sub_word_start`
483            // * `move_next_sub_word_end`
484            // * `move_prev_sub_word_start`
485            // * `move_prev_sub_word_end`
486            Motion::NextSubwordStart { ignore_punctuation } => {
487                let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, false);
488                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
489            }
490            Motion::NextSubwordEnd { ignore_punctuation } => {
491                let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, false);
492                self.helix_find_range_forward(times, window, cx, &mut is_boundary)
493            }
494            Motion::PreviousSubwordStart { ignore_punctuation } => {
495                let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, true);
496                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
497            }
498            Motion::PreviousSubwordEnd { ignore_punctuation } => {
499                let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, true);
500                self.helix_find_range_backward(times, window, cx, &mut is_boundary)
501            }
502            Motion::StartOfDocument => {
503                self.update_editor(cx, |_, editor, cx| {
504                    editor.change_selections(Default::default(), window, cx, |s| {
505                        s.move_with(&mut |map, selection| {
506                            selection
507                                .collapse_to(start_of_document(map, times), SelectionGoal::None)
508                        })
509                    });
510                });
511            }
512            Motion::EndOfDocument => {
513                self.update_editor(cx, |_, editor, cx| {
514                    editor.change_selections(Default::default(), window, cx, |s| {
515                        s.move_with(&mut |map, selection| {
516                            selection.collapse_to(end_of_document(map), SelectionGoal::None)
517                        })
518                    });
519                });
520            }
521            Motion::EndOfLine { .. } => {
522                // In Helix mode, EndOfLine should position cursor ON the last character,
523                // not after it. We therefore need special handling for it.
524                self.update_editor(cx, |_, editor, cx| {
525                    let text_layout_details = editor.text_layout_details(window, cx);
526                    editor.change_selections(Default::default(), window, cx, |s| {
527                        s.move_with(&mut |map, selection| {
528                            let goal = selection.goal;
529                            let cursor = if selection.is_empty() || selection.reversed {
530                                selection.head()
531                            } else {
532                                movement::left(map, selection.head())
533                            };
534
535                            let (point, _goal) = motion
536                                .move_point(map, cursor, goal, times, &text_layout_details)
537                                .unwrap_or((cursor, goal));
538
539                            // Move left by one character to position on the last character
540                            let adjusted_point = movement::saturating_left(map, point);
541                            selection.collapse_to(adjusted_point, SelectionGoal::None)
542                        })
543                    });
544                });
545            }
546            Motion::FindForward {
547                before,
548                char,
549                mode,
550                smartcase,
551            } => {
552                self.helix_new_selections(window, cx, &mut |cursor, map| {
553                    let start = cursor;
554                    let mut last_boundary = start;
555                    for _ in 0..times.unwrap_or(1) {
556                        last_boundary = movement::find_boundary(
557                            map,
558                            movement::right(map, last_boundary),
559                            mode,
560                            &mut |left, right| {
561                                let current_char = if before { right } else { left };
562                                motion::is_character_match(char, current_char, smartcase)
563                            },
564                        );
565                    }
566                    Some((last_boundary, start))
567                });
568            }
569            Motion::FindBackward {
570                after,
571                char,
572                mode,
573                smartcase,
574            } => {
575                self.helix_new_selections(window, cx, &mut |cursor, map| {
576                    let start = cursor;
577                    let mut last_boundary = start;
578                    for _ in 0..times.unwrap_or(1) {
579                        last_boundary = movement::find_preceding_boundary_display_point(
580                            map,
581                            last_boundary,
582                            mode,
583                            &mut |left, right| {
584                                let current_char = if after { left } else { right };
585                                motion::is_character_match(char, current_char, smartcase)
586                            },
587                        );
588                    }
589                    // The original cursor was one character wide,
590                    // but the search started from the left side of it,
591                    // so to include that space the selection must end one character to the right.
592                    Some((last_boundary, movement::right(map, start)))
593                });
594            }
595            _ => self.helix_move_and_collapse(motion, times, window, cx),
596        }
597    }
598
599    pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
600        self.update_editor(cx, |vim, editor, cx| {
601            let has_selection = editor
602                .selections
603                .all_adjusted(&editor.display_snapshot(cx))
604                .iter()
605                .any(|selection| !selection.is_empty());
606
607            if !has_selection {
608                // If no selection, expand to current character (like 'v' does)
609                editor.change_selections(Default::default(), window, cx, |s| {
610                    s.move_with(&mut |map, selection| {
611                        let head = selection.head();
612                        let new_head = movement::saturating_right(map, head);
613                        selection.set_tail(head, SelectionGoal::None);
614                        selection.set_head(new_head, SelectionGoal::None);
615                    });
616                });
617                vim.yank_selections_content(
618                    editor,
619                    crate::motion::MotionKind::Exclusive,
620                    window,
621                    cx,
622                );
623                editor.change_selections(Default::default(), window, cx, |s| {
624                    s.move_with(&mut |_map, selection| {
625                        selection.collapse_to(selection.start, SelectionGoal::None);
626                    });
627                });
628            } else {
629                // Yank the selection(s)
630                vim.yank_selections_content(
631                    editor,
632                    crate::motion::MotionKind::Exclusive,
633                    window,
634                    cx,
635                );
636            }
637        });
638
639        // Drop back to normal mode after yanking
640        self.switch_mode(Mode::HelixNormal, true, window, cx);
641    }
642
643    fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
644        self.start_recording(cx);
645        self.update_editor(cx, |_, editor, cx| {
646            editor.change_selections(Default::default(), window, cx, |s| {
647                s.move_with(&mut |_map, selection| {
648                    // In helix normal mode, move cursor to start of selection and collapse
649                    if !selection.is_empty() {
650                        selection.collapse_to(selection.start, SelectionGoal::None);
651                    }
652                });
653            });
654        });
655        self.switch_mode(Mode::Insert, false, window, cx);
656    }
657
658    fn helix_select_regex(
659        &mut self,
660        _: &HelixSelectRegex,
661        window: &mut Window,
662        cx: &mut Context<Self>,
663    ) {
664        Vim::take_forced_motion(cx);
665        let Some(pane) = self.pane(window, cx) else {
666            return;
667        };
668        let prior_selections = self.editor_selections(window, cx);
669        pane.update(cx, |pane, cx| {
670            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
671                search_bar.update(cx, |search_bar, cx| {
672                    if !search_bar.show(window, cx) {
673                        return;
674                    }
675
676                    search_bar.select_query(window, cx);
677                    cx.focus_self(window);
678
679                    search_bar.set_replacement(None, cx);
680                    let mut options = SearchOptions::NONE;
681                    options |= SearchOptions::REGEX;
682                    if EditorSettings::get_global(cx).search.case_sensitive {
683                        options |= SearchOptions::CASE_SENSITIVE;
684                    }
685                    search_bar.set_search_options(options, cx);
686                    if let Some(search) = search_bar.set_search_within_selection(
687                        Some(FilteredSearchRange::Selection),
688                        window,
689                        cx,
690                    ) {
691                        cx.spawn_in(window, async move |search_bar, cx| {
692                            if search.await.is_ok() {
693                                search_bar.update_in(cx, |search_bar, window, cx| {
694                                    search_bar.activate_current_match(window, cx)
695                                })
696                            } else {
697                                Ok(())
698                            }
699                        })
700                        .detach_and_log_err(cx);
701                    }
702                    self.search = SearchState {
703                        direction: searchable::Direction::Next,
704                        count: 1,
705                        cmd_f_search: false,
706                        prior_selections,
707                        prior_operator: self.operator_stack.last().cloned(),
708                        prior_mode: self.mode,
709                        helix_select: true,
710                        _dismiss_subscription: None,
711                    }
712                });
713            }
714        });
715        self.start_recording(cx);
716    }
717
718    fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
719        self.start_recording(cx);
720        self.switch_mode(Mode::Insert, false, window, cx);
721        self.update_editor(cx, |_, editor, cx| {
722            editor.change_selections(Default::default(), window, cx, |s| {
723                s.move_with(&mut |map, selection| {
724                    let point = if selection.is_empty() {
725                        right(map, selection.head(), 1)
726                    } else {
727                        selection.end
728                    };
729                    selection.collapse_to(point, SelectionGoal::None);
730                });
731            });
732        });
733    }
734
735    /// Helix-specific implementation of `shift-a` that accounts for Helix's
736    /// selection model, where selecting a line with `x` creates a selection
737    /// from column 0 of the current row to column 0 of the next row, so the
738    /// default [`vim::normal::InsertEndOfLine`] would move the cursor to the
739    /// end of the wrong line.
740    fn helix_insert_end_of_line(
741        &mut self,
742        _: &HelixInsertEndOfLine,
743        window: &mut Window,
744        cx: &mut Context<Self>,
745    ) {
746        self.start_recording(cx);
747        self.switch_mode(Mode::Insert, false, window, cx);
748        self.update_editor(cx, |_, editor, cx| {
749            editor.change_selections(Default::default(), window, cx, |s| {
750                s.move_with(&mut |map, selection| {
751                    let cursor = if !selection.is_empty() && !selection.reversed {
752                        movement::left(map, selection.head())
753                    } else {
754                        selection.head()
755                    };
756                    selection
757                        .collapse_to(motion::next_line_end(map, cursor, 1), SelectionGoal::None);
758                });
759            });
760        });
761    }
762
763    pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
764        self.update_editor(cx, |_, editor, cx| {
765            editor.transact(window, cx, |editor, window, cx| {
766                let display_map = editor.display_snapshot(cx);
767                let selections = editor.selections.all_display(&display_map);
768
769                let mut edits = Vec::new();
770                let mut selection_info = Vec::new();
771                for selection in &selections {
772                    let mut range = selection.range();
773                    let was_empty = range.is_empty();
774                    let was_reversed = selection.reversed;
775
776                    if was_empty {
777                        range.end = movement::saturating_right(&display_map, range.start);
778                    }
779
780                    let byte_range = range.start.to_offset(&display_map, Bias::Left)
781                        ..range.end.to_offset(&display_map, Bias::Left);
782
783                    let snapshot = display_map.buffer_snapshot();
784                    let grapheme_count = snapshot.grapheme_count_for_range(&byte_range);
785                    let anchor = snapshot.anchor_before(byte_range.start);
786                    let mut replacement_len = 0;
787
788                    if !byte_range.is_empty() {
789                        let mut replacement_text = text.repeat(grapheme_count);
790                        LineEnding::normalize(&mut replacement_text);
791                        replacement_len = replacement_text.len();
792                        edits.push((byte_range, replacement_text));
793                    }
794
795                    selection_info.push((anchor, replacement_len, was_empty, was_reversed));
796                }
797
798                editor.edit(edits, cx);
799
800                // Restore selections based on original info
801                let snapshot = editor.buffer().read(cx).snapshot(cx);
802                let ranges: Vec<_> = selection_info
803                    .into_iter()
804                    .map(|(start_anchor, replacement_len, was_empty, was_reversed)| {
805                        let start_point = start_anchor.to_point(&snapshot);
806                        if was_empty {
807                            start_point..start_point
808                        } else {
809                            let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
810                            let end_point = snapshot.offset_to_point(end_offset);
811                            if was_reversed {
812                                end_point..start_point
813                            } else {
814                                start_point..end_point
815                            }
816                        }
817                    })
818                    .collect();
819
820                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
821                    s.select_ranges(ranges);
822                });
823            });
824        });
825        self.switch_mode(Mode::HelixNormal, true, window, cx);
826    }
827
828    pub fn helix_goto_last_modification(
829        &mut self,
830        _: &HelixGotoLastModification,
831        window: &mut Window,
832        cx: &mut Context<Self>,
833    ) {
834        self.jump(".".into(), false, false, window, cx);
835    }
836
837    pub fn helix_select_lines(
838        &mut self,
839        _: &HelixSelectLine,
840        window: &mut Window,
841        cx: &mut Context<Self>,
842    ) {
843        let count = Vim::take_count(cx).unwrap_or(1);
844        self.update_editor(cx, |_, editor, cx| {
845            let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
846            let mut selections = editor.selections.all::<Point>(&display_map);
847            let max_point = display_map.buffer_snapshot().max_point();
848            let buffer_snapshot = &display_map.buffer_snapshot();
849
850            for selection in &mut selections {
851                // Start always goes to column 0 of the first selected line
852                let start_row = selection.start.row;
853                let current_end_row = selection.end.row;
854
855                // Check if cursor is on empty line by checking first character
856                let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
857                let first_char = buffer_snapshot.chars_at(line_start_offset).next();
858                let extra_line = if first_char == Some('\n') && selection.is_empty() {
859                    1
860                } else {
861                    0
862                };
863
864                let end_row = current_end_row + count as u32 + extra_line;
865
866                selection.start = Point::new(start_row, 0);
867                selection.end = if end_row > max_point.row {
868                    max_point
869                } else {
870                    Point::new(end_row, 0)
871                };
872                selection.reversed = false;
873            }
874
875            editor.change_selections(Default::default(), window, cx, |s| {
876                s.select(selections);
877            });
878        });
879    }
880
881    fn helix_keep_newest_selection(
882        &mut self,
883        _: &HelixKeepNewestSelection,
884        window: &mut Window,
885        cx: &mut Context<Self>,
886    ) {
887        self.update_editor(cx, |_, editor, cx| {
888            let newest = editor
889                .selections
890                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
891            editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
892        });
893    }
894
895    fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
896        self.update_editor(cx, |vim, editor, cx| {
897            editor.set_clip_at_line_ends(false, cx);
898            editor.transact(window, cx, |editor, window, cx| {
899                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
900                    s.move_with(&mut |map, selection| {
901                        if selection.start == selection.end {
902                            selection.end = movement::right(map, selection.end);
903                        }
904
905                        // If the selection starts and ends on a newline, we exclude the last one.
906                        if !selection.is_empty()
907                            && selection.start.column() == 0
908                            && selection.end.column() == 0
909                        {
910                            selection.end = movement::left(map, selection.end);
911                        }
912                    })
913                });
914                if yank {
915                    vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
916                }
917                let selections = editor
918                    .selections
919                    .all::<Point>(&editor.display_snapshot(cx))
920                    .into_iter();
921                let edits = selections.map(|selection| (selection.start..selection.end, ""));
922                editor.edit(edits, cx);
923            });
924        });
925        self.switch_mode(Mode::Insert, true, window, cx);
926    }
927
928    fn helix_substitute(
929        &mut self,
930        _: &HelixSubstitute,
931        window: &mut Window,
932        cx: &mut Context<Self>,
933    ) {
934        self.do_helix_substitute(true, window, cx);
935    }
936
937    fn helix_substitute_no_yank(
938        &mut self,
939        _: &HelixSubstituteNoYank,
940        window: &mut Window,
941        cx: &mut Context<Self>,
942    ) {
943        self.do_helix_substitute(false, window, cx);
944    }
945
946    fn helix_select_next(
947        &mut self,
948        _: &HelixSelectNext,
949        window: &mut Window,
950        cx: &mut Context<Self>,
951    ) {
952        self.do_helix_select(Direction::Next, window, cx);
953    }
954
955    fn helix_select_previous(
956        &mut self,
957        _: &HelixSelectPrevious,
958        window: &mut Window,
959        cx: &mut Context<Self>,
960    ) {
961        self.do_helix_select(Direction::Prev, window, cx);
962    }
963
964    fn do_helix_select(
965        &mut self,
966        direction: searchable::Direction,
967        window: &mut Window,
968        cx: &mut Context<Self>,
969    ) {
970        let Some(pane) = self.pane(window, cx) else {
971            return;
972        };
973        let count = Vim::take_count(cx).unwrap_or(1);
974        Vim::take_forced_motion(cx);
975        let prior_selections = self.editor_selections(window, cx);
976
977        let success = pane.update(cx, |pane, cx| {
978            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
979                return false;
980            };
981            search_bar.update(cx, |search_bar, cx| {
982                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
983                    return false;
984                }
985                search_bar.select_match(direction, count, window, cx);
986                true
987            })
988        });
989
990        if !success {
991            return;
992        }
993        if self.mode == Mode::HelixSelect {
994            self.update_editor(cx, |_vim, editor, cx| {
995                let snapshot = editor.snapshot(window, cx);
996                editor.change_selections(SelectionEffects::default(), window, cx, |s| {
997                    let buffer = snapshot.buffer_snapshot();
998
999                    s.select_ranges(
1000                        prior_selections
1001                            .iter()
1002                            .cloned()
1003                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
1004                            .map(|range| {
1005                                let start = range.start.to_offset(buffer);
1006                                let end = range.end.to_offset(buffer);
1007                                start..end
1008                            }),
1009                    );
1010                })
1011            });
1012        }
1013    }
1014
1015    pub fn helix_jump_to_word(
1016        &mut self,
1017        _: &HelixJumpToWord,
1018        window: &mut Window,
1019        cx: &mut Context<Self>,
1020    ) {
1021        let behaviour = match self.mode {
1022            // Vim normal mode treats jump-to-word as a cursor motion, while Helix
1023            // normal mode treats the cursor as a single-character selection.
1024            Mode::Normal => HelixJumpBehaviour::MoveToWordStart,
1025            // Vim visual mode extends like a motion, so the cursor stops at the
1026            // same word boundary as normal mode instead of selecting the word.
1027            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
1028                HelixJumpBehaviour::ExtendToWordStart
1029            }
1030            Mode::HelixSelect => HelixJumpBehaviour::Extend,
1031            _ => HelixJumpBehaviour::Move,
1032        };
1033        self.start_helix_jump(behaviour, window, cx);
1034    }
1035
1036    fn start_helix_jump(
1037        &mut self,
1038        behaviour: HelixJumpBehaviour,
1039        window: &mut Window,
1040        cx: &mut Context<Self>,
1041    ) {
1042        let allow_targets_in_selection = self.mode.has_selection();
1043        let Some(data) = self.collect_helix_jump_data(allow_targets_in_selection, window, cx)
1044        else {
1045            return;
1046        };
1047
1048        if data.labels.is_empty() {
1049            self.clear_helix_jump_ui(window, cx);
1050            return;
1051        }
1052
1053        if !self.apply_helix_jump_ui(data.overlays, window, cx) {
1054            return;
1055        }
1056
1057        self.push_operator(
1058            Operator::HelixJump {
1059                behaviour,
1060                first_char: None,
1061                labels: data.labels,
1062            },
1063            window,
1064            cx,
1065        );
1066    }
1067
1068    fn collect_helix_jump_data(
1069        &mut self,
1070        allow_targets_in_selection: bool,
1071        window: &mut Window,
1072        cx: &mut Context<Self>,
1073    ) -> Option<HelixJumpUiData> {
1074        self.update_editor(cx, |_, editor, cx| {
1075            let snapshot = editor.snapshot(window, cx);
1076            let display_snapshot = &snapshot.display_snapshot;
1077            let buffer_snapshot = display_snapshot.buffer_snapshot();
1078            let visible_range = Self::visible_jump_range(editor, &snapshot, display_snapshot, cx);
1079            let start_offset = buffer_snapshot.point_to_offset(visible_range.start);
1080            let end_offset = buffer_snapshot.point_to_offset(visible_range.end);
1081
1082            let selections = editor.selections.all::<Point>(&display_snapshot);
1083            let skip_data = Self::selection_skip_offsets(
1084                buffer_snapshot,
1085                &selections,
1086                allow_targets_in_selection,
1087            );
1088
1089            // Get the primary cursor position for alternating forward/backward labeling
1090            let cursor_offset = selections
1091                .first()
1092                .map(|s| buffer_snapshot.point_to_offset(s.head()))
1093                .unwrap_or(start_offset);
1094
1095            let style = editor.style(cx);
1096            let font = style.text.font();
1097            let font_size = style.text.font_size.to_pixels(window.rem_size());
1098            let label_color = cx.theme().colors().vim_helix_jump_label_foreground;
1099
1100            Self::build_helix_jump_ui_data(
1101                buffer_snapshot,
1102                start_offset,
1103                end_offset,
1104                cursor_offset,
1105                label_color,
1106                &skip_data,
1107                window.text_system(),
1108                font,
1109                font_size,
1110            )
1111        })
1112    }
1113
1114    fn visible_jump_range(
1115        editor: &Editor,
1116        snapshot: &editor::EditorSnapshot,
1117        display_snapshot: &DisplaySnapshot,
1118        cx: &App,
1119    ) -> Range<Point> {
1120        let visible_range = editor.multi_buffer_visible_range(display_snapshot, cx);
1121        if editor.visible_line_count().is_some() || visible_range.start != visible_range.end {
1122            return visible_range;
1123        }
1124
1125        let scroll_position = snapshot.scroll_position();
1126        let top_row = scroll_position.y.floor().max(0.0) as u32;
1127        let visible_rows = display_snapshot
1128            .max_point()
1129            .row()
1130            .0
1131            .saturating_sub(top_row)
1132            .saturating_add(1);
1133        let start_display_point = DisplayPoint::new(DisplayRow(top_row), 0);
1134        let end_display_point =
1135            DisplayPoint::new(DisplayRow(top_row.saturating_add(visible_rows)), 0);
1136
1137        display_snapshot.display_point_to_point(start_display_point, Bias::Left)
1138            ..display_snapshot.display_point_to_point(end_display_point, Bias::Right)
1139    }
1140
1141    fn build_helix_jump_ui_data(
1142        buffer: &MultiBufferSnapshot,
1143        start_offset: MultiBufferOffset,
1144        end_offset: MultiBufferOffset,
1145        cursor_offset: MultiBufferOffset,
1146        label_color: Hsla,
1147        skip_data: &HelixJumpSkipData,
1148        text_system: &WindowTextSystem,
1149        font: Font,
1150        font_size: Pixels,
1151    ) -> HelixJumpUiData {
1152        if start_offset >= end_offset {
1153            return HelixJumpUiData::default();
1154        }
1155
1156        // First pass: collect all word candidates without assigning labels
1157        let candidates = Self::collect_jump_candidates(buffer, start_offset, end_offset, skip_data);
1158
1159        if candidates.is_empty() {
1160            return HelixJumpUiData::default();
1161        }
1162
1163        let ordered_candidates = Self::order_jump_candidates(candidates, cursor_offset);
1164
1165        // Now assign labels and build UI data
1166        let mut labels = Vec::with_capacity(ordered_candidates.len());
1167        let mut overlays = Vec::with_capacity(ordered_candidates.len());
1168
1169        let width_of = |text: &str| -> Pixels {
1170            if text.is_empty() {
1171                return px(0.0);
1172            }
1173
1174            let run = gpui::TextRun {
1175                len: text.len(),
1176                font: font.clone(),
1177                color: Hsla::default(),
1178                background_color: None,
1179                underline: None,
1180                strikethrough: None,
1181            };
1182
1183            text_system.layout_line(text, font_size, &[run], None).width
1184        };
1185
1186        let is_monospace = Self::is_monospace_jump_font(text_system, &font, font_size);
1187
1188        for (label_index, candidate) in ordered_candidates.into_iter().enumerate() {
1189            let start_anchor = buffer.anchor_after(candidate.word_start);
1190            let end_anchor = buffer.anchor_after(candidate.word_end);
1191            let label = Self::jump_label_for_index(label_index);
1192            let label_text = label.iter().collect::<String>();
1193            // Monospace fonts: the label always matches the width of the first two characters,
1194            // so no per-word measurement is needed.
1195            // Proportional fonts: a label like "mw" can be wider than a short word like "if",
1196            // so we hide enough of the word (and possibly trailing whitespace) to make room,
1197            // or shift the label left into preceding whitespace.
1198            let fit = if is_monospace {
1199                JumpLabelFit::monospace(candidate.first_two_end)
1200            } else {
1201                let label_width = width_of(&label_text);
1202                Self::fit_proportional_jump_label(
1203                    buffer,
1204                    &candidate,
1205                    end_offset,
1206                    label_width,
1207                    &width_of,
1208                )
1209            };
1210
1211            let hide_end_anchor = buffer.anchor_after(fit.hide_end_offset);
1212
1213            labels.push(HelixJumpLabel {
1214                label,
1215                range: start_anchor..end_anchor,
1216            });
1217
1218            overlays.push(NavigationTargetOverlay {
1219                target_range: start_anchor..end_anchor,
1220                label: NavigationOverlayLabel {
1221                    text: label_text.into(),
1222                    text_color: label_color,
1223                    x_offset: -fit.left_shift,
1224                    scale_factor: fit.scale_factor,
1225                },
1226                covered_text_range: Some(start_anchor..hide_end_anchor),
1227            });
1228        }
1229
1230        HelixJumpUiData { labels, overlays }
1231    }
1232
1233    fn collect_jump_candidates(
1234        buffer: &MultiBufferSnapshot,
1235        start_offset: MultiBufferOffset,
1236        end_offset: MultiBufferOffset,
1237        skip_data: &HelixJumpSkipData,
1238    ) -> Vec<JumpCandidate> {
1239        let mut candidates = Vec::new();
1240
1241        let mut offset = start_offset;
1242        let mut in_word = false;
1243        let mut word_start = start_offset;
1244        let mut first_two_end = start_offset;
1245        let mut char_count = 0;
1246
1247        for chunk in buffer.text_for_range(start_offset..end_offset) {
1248            for (idx, ch) in chunk.char_indices() {
1249                let absolute = offset + idx;
1250                let is_word = is_jump_word_char(ch);
1251                if is_word {
1252                    if !in_word {
1253                        in_word = true;
1254                        word_start = absolute;
1255                        char_count = 0;
1256                    }
1257                    if char_count == 1 {
1258                        first_two_end = absolute + ch.len_utf8();
1259                    }
1260                    char_count += 1;
1261                }
1262
1263                if !is_word && in_word {
1264                    if char_count >= 2
1265                        && !Self::should_skip_jump_candidate(word_start, absolute, skip_data)
1266                    {
1267                        candidates.push(JumpCandidate {
1268                            word_start,
1269                            word_end: absolute,
1270                            first_two_end,
1271                        });
1272                    }
1273                    in_word = false;
1274                }
1275            }
1276            offset += chunk.len();
1277        }
1278
1279        // Handle word at end of buffer
1280        if in_word
1281            && char_count >= 2
1282            && !Self::should_skip_jump_candidate(word_start, end_offset, skip_data)
1283        {
1284            candidates.push(JumpCandidate {
1285                word_start,
1286                word_end: end_offset,
1287                first_two_end,
1288            });
1289        }
1290
1291        candidates
1292    }
1293
1294    fn selection_skip_offsets(
1295        buffer: &MultiBufferSnapshot,
1296        selections: &[Selection<Point>],
1297        allow_targets_in_selection: bool,
1298    ) -> HelixJumpSkipData {
1299        let mut skip_points = Vec::with_capacity(selections.len());
1300        let mut skip_ranges = Vec::new();
1301
1302        for selection in selections {
1303            let head_offset = buffer.point_to_offset(selection.head());
1304            skip_points.push(head_offset);
1305
1306            if !allow_targets_in_selection && selection.start != selection.end {
1307                let mut start = buffer.point_to_offset(selection.start);
1308                let mut end = buffer.point_to_offset(selection.end);
1309                if start > end {
1310                    std::mem::swap(&mut start, &mut end);
1311                }
1312                skip_ranges.push(start..end);
1313            }
1314        }
1315
1316        skip_points.sort_unstable();
1317
1318        skip_ranges.sort_unstable_by_key(|range| range.start);
1319        let mut merged_ranges: Vec<Range<MultiBufferOffset>> =
1320            Vec::with_capacity(skip_ranges.len());
1321        for range in skip_ranges {
1322            if let Some(previous_range) = merged_ranges.last_mut()
1323                && range.start <= previous_range.end
1324            {
1325                previous_range.end = previous_range.end.max(range.end);
1326            } else {
1327                merged_ranges.push(range);
1328            }
1329        }
1330
1331        HelixJumpSkipData {
1332            points: skip_points,
1333            ranges: merged_ranges,
1334        }
1335    }
1336
1337    fn should_skip_jump_candidate(
1338        word_start: MultiBufferOffset,
1339        word_end: MultiBufferOffset,
1340        skip_data: &HelixJumpSkipData,
1341    ) -> bool {
1342        // word_end is exclusive, so points at the following delimiter should not skip the word.
1343        let point_index = skip_data
1344            .points
1345            .partition_point(|offset| *offset < word_start);
1346        if skip_data
1347            .points
1348            .get(point_index)
1349            .is_some_and(|offset| *offset < word_end)
1350        {
1351            return true;
1352        }
1353
1354        let range_index = skip_data
1355            .ranges
1356            .partition_point(|range| range.end <= word_start);
1357        skip_data
1358            .ranges
1359            .get(range_index)
1360            .is_some_and(|range| range.start < word_end)
1361    }
1362
1363    /// Interleave candidates so forward targets get even label indices (aa, ac, ae...)
1364    /// and backward targets get odd indices (ab, ad, af...), matching Helix's algorithm.
1365    /// This keeps the earliest label assignments close to the cursor in both directions.
1366    fn order_jump_candidates(
1367        candidates: Vec<JumpCandidate>,
1368        cursor_offset: MultiBufferOffset,
1369    ) -> Vec<JumpCandidate> {
1370        let mut forward = Vec::with_capacity(candidates.len());
1371        let mut backward = Vec::new();
1372
1373        for candidate in candidates {
1374            if candidate.word_start < cursor_offset {
1375                backward.push(candidate);
1376            } else {
1377                forward.push(candidate);
1378            }
1379        }
1380
1381        backward.reverse();
1382
1383        let mut ordered_candidates =
1384            Vec::with_capacity((forward.len() + backward.len()).min(HELIX_JUMP_LABEL_LIMIT));
1385        let mut forward_candidates = forward.into_iter();
1386        let mut backward_candidates = backward.into_iter();
1387
1388        loop {
1389            let mut pushed_candidate = false;
1390
1391            if ordered_candidates.len() < HELIX_JUMP_LABEL_LIMIT
1392                && let Some(candidate) = forward_candidates.next()
1393            {
1394                ordered_candidates.push(candidate);
1395                pushed_candidate = true;
1396            }
1397
1398            if ordered_candidates.len() < HELIX_JUMP_LABEL_LIMIT
1399                && let Some(candidate) = backward_candidates.next()
1400            {
1401                ordered_candidates.push(candidate);
1402                pushed_candidate = true;
1403            }
1404
1405            if !pushed_candidate {
1406                break;
1407            }
1408        }
1409
1410        ordered_candidates
1411    }
1412
1413    fn jump_label_for_index(index: usize) -> [char; 2] {
1414        [
1415            HELIX_JUMP_ALPHABET[index / HELIX_JUMP_ALPHABET.len()],
1416            HELIX_JUMP_ALPHABET[index % HELIX_JUMP_ALPHABET.len()],
1417        ]
1418    }
1419
1420    fn is_monospace_jump_font(
1421        text_system: &WindowTextSystem,
1422        font: &Font,
1423        font_size: Pixels,
1424    ) -> bool {
1425        let font_id = text_system.resolve_font(font);
1426        let width_of_char = |ch| {
1427            text_system
1428                .advance(font_id, font_size, ch)
1429                .map(|size| size.width)
1430                .unwrap_or_else(|_| text_system.layout_width(font_id, font_size, ch))
1431        };
1432
1433        let a = width_of_char('i');
1434        let b = width_of_char('w');
1435        let c = width_of_char('0');
1436        let d = width_of_char('1');
1437        let diff_1 = if a > b { a - b } else { b - a };
1438        let diff_2 = if c > d { c - d } else { d - c };
1439        diff_1 <= HELIX_JUMP_MONOSPACE_TOLERANCE && diff_2 <= HELIX_JUMP_MONOSPACE_TOLERANCE
1440    }
1441
1442    /// Fit a jump label over a word in a proportional font.
1443    ///
1444    /// Prefer fitting within the word itself, using available whitespace to the left
1445    /// before consuming trailing whitespace after the word. If the label still cannot
1446    /// fit cleanly, allow a small amount of scaling.
1447    fn fit_proportional_jump_label<F: Fn(&str) -> Pixels>(
1448        buffer: &MultiBufferSnapshot,
1449        candidate: &JumpCandidate,
1450        end_offset: MultiBufferOffset,
1451        label_width: Pixels,
1452        width_of: &F,
1453    ) -> JumpLabelFit {
1454        let fit_budget = Self::jump_label_fit_budget(buffer, candidate, end_offset, width_of);
1455
1456        let mut hidden_prefix = HiddenPrefixFitState::new(candidate.first_two_end);
1457        let min_label_scale = if fit_budget.preserve_full_scale {
1458            1.0
1459        } else {
1460            HELIX_JUMP_MIN_LABEL_SCALE
1461        };
1462
1463        hidden_prefix.extend_to_fit(
1464            buffer,
1465            candidate.word_start,
1466            candidate.word_end,
1467            candidate.word_end,
1468            label_width,
1469            fit_budget.max_left_shift,
1470            min_label_scale,
1471            width_of,
1472        );
1473
1474        if label_width > px(0.0)
1475            && hidden_prefix.needs_more_width(label_width, fit_budget.max_left_shift)
1476            && fit_budget.allowed_trailing_hide_end > candidate.word_end
1477        {
1478            hidden_prefix.extend_to_fit(
1479                buffer,
1480                candidate.word_end,
1481                fit_budget.allowed_trailing_hide_end,
1482                candidate.word_end,
1483                label_width,
1484                fit_budget.max_left_shift,
1485                min_label_scale,
1486                width_of,
1487            );
1488        }
1489
1490        // Jump candidates always contain at least two word characters, and the initial
1491        // scan above always measures through that second character before we read the width.
1492        let hidden_width = hidden_prefix.hidden_width;
1493
1494        let left_shift = if label_width > hidden_width {
1495            (label_width - hidden_width).min(fit_budget.max_left_shift)
1496        } else {
1497            px(0.0)
1498        };
1499
1500        let scale_factor = if label_width > px(0.0) {
1501            let scale = ((hidden_width + left_shift) / label_width).min(1.0);
1502            if scale < 1.0 { scale * 0.99 } else { 1.0 }
1503        } else {
1504            1.0
1505        };
1506
1507        JumpLabelFit {
1508            hide_end_offset: hidden_prefix.hide_end_offset,
1509            left_shift,
1510            scale_factor: if fit_budget.preserve_full_scale {
1511                1.0
1512            } else {
1513                scale_factor
1514            },
1515        }
1516    }
1517
1518    fn jump_label_fit_budget<F: Fn(&str) -> Pixels>(
1519        buffer: &MultiBufferSnapshot,
1520        candidate: &JumpCandidate,
1521        end_offset: MultiBufferOffset,
1522        width_of: &F,
1523    ) -> JumpLabelFitBudget {
1524        let mut left_ws_rev = String::new();
1525        let mut left_ws_count = 0usize;
1526        let mut left_stopped_at_line_break = false;
1527        let mut left_stopped_at_non_ws = false;
1528        let mut left_hit_limit = false;
1529
1530        for ch in buffer.reversed_chars_at(candidate.word_start) {
1531            if ch == '\n' || ch == '\r' {
1532                left_stopped_at_line_break = true;
1533                break;
1534            }
1535
1536            if !ch.is_whitespace() {
1537                left_stopped_at_non_ws = true;
1538                break;
1539            }
1540
1541            left_ws_count += 1;
1542            if left_ws_count > HELIX_JUMP_MAX_LEFT_WS_CHARS {
1543                left_hit_limit = true;
1544                break;
1545            }
1546
1547            left_ws_rev.push(ch);
1548        }
1549
1550        let left_ws: String = left_ws_rev.chars().rev().collect();
1551        let left_ws_width = width_of(&left_ws);
1552        let left_is_indentation =
1553            left_stopped_at_line_break || (!left_stopped_at_non_ws && !left_hit_limit);
1554        // Between tokens leave a small gap so the label doesn't touch the previous word;
1555        // for line-leading indentation the full width is safe.
1556        let min_left_gap = if left_is_indentation {
1557            px(0.0)
1558        } else {
1559            px(2.0)
1560        };
1561        let max_left_shift = (left_ws_width - min_left_gap).max(px(0.0));
1562
1563        let mut allowed_trailing_hide_end = candidate.word_end;
1564        let mut ws_count = 0usize;
1565        let mut last_ws_start = candidate.word_end;
1566        let mut ws_end_offset = candidate.word_end;
1567        let mut next_non_ws = None;
1568        let mut hit_line_break_after_word = false;
1569
1570        let mut ws_scan_offset = candidate.word_end;
1571        'scan: for chunk in buffer.text_for_range(candidate.word_end..end_offset) {
1572            for (idx, ch) in chunk.char_indices() {
1573                let absolute = ws_scan_offset + idx;
1574                if ch == '\n' || ch == '\r' {
1575                    hit_line_break_after_word = true;
1576                    break 'scan;
1577                }
1578                if !ch.is_whitespace() {
1579                    next_non_ws = Some(ch);
1580                    break 'scan;
1581                }
1582
1583                ws_count += 1;
1584                last_ws_start = absolute;
1585                ws_end_offset = absolute + ch.len_utf8();
1586            }
1587            ws_scan_offset += chunk.len();
1588        }
1589
1590        let preserve_full_scale = hit_line_break_after_word && next_non_ws.is_none()
1591            || matches!(
1592                buffer.chars_at(candidate.word_end).next(),
1593                None | Some('\n') | Some('\r')
1594            );
1595
1596        if ws_count > 0 {
1597            let next_is_word = match next_non_ws {
1598                Some(ch) => is_jump_word_char(ch),
1599                None => false,
1600            };
1601
1602            if next_is_word {
1603                // Keep at least one whitespace character visible so adjacent labels
1604                // remain visually separated.
1605                if ws_count > 1 {
1606                    allowed_trailing_hide_end = last_ws_start;
1607                }
1608            } else {
1609                // Next token is punctuation or end-of-range — safe to hide all whitespace.
1610                allowed_trailing_hide_end = ws_end_offset;
1611            }
1612        }
1613
1614        JumpLabelFitBudget {
1615            max_left_shift,
1616            allowed_trailing_hide_end,
1617            preserve_full_scale,
1618        }
1619    }
1620}
1621
1622const HELIX_JUMP_ALPHABET: &[char; 26] = &[
1623    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
1624    't', 'u', 'v', 'w', 'x', 'y', 'z',
1625];
1626const HELIX_JUMP_LABEL_LIMIT: usize = HELIX_JUMP_ALPHABET.len() * HELIX_JUMP_ALPHABET.len();
1627const HELIX_JUMP_MONOSPACE_TOLERANCE: Pixels = px(0.5);
1628const HELIX_JUMP_MIN_LABEL_SCALE: f32 = 1.0;
1629const HELIX_JUMP_MAX_HIDDEN_CHARS: usize = 16;
1630const HELIX_JUMP_MAX_LEFT_WS_CHARS: usize = 32;
1631
1632fn is_jump_word_char(ch: char) -> bool {
1633    ch == '_' || ch.is_alphanumeric()
1634}
1635
1636/// A word candidate for jump labels, before label assignment.
1637#[derive(Clone)]
1638struct JumpCandidate {
1639    word_start: MultiBufferOffset,
1640    word_end: MultiBufferOffset,
1641    first_two_end: MultiBufferOffset,
1642}
1643
1644struct HelixJumpSkipData {
1645    points: Vec<MultiBufferOffset>,
1646    ranges: Vec<Range<MultiBufferOffset>>,
1647}
1648
1649struct JumpLabelFit {
1650    hide_end_offset: MultiBufferOffset,
1651    left_shift: Pixels,
1652    scale_factor: f32,
1653}
1654
1655struct JumpLabelFitBudget {
1656    max_left_shift: Pixels,
1657    allowed_trailing_hide_end: MultiBufferOffset,
1658    preserve_full_scale: bool,
1659}
1660
1661struct HiddenPrefixFitState {
1662    text: String,
1663    hide_end_offset: MultiBufferOffset,
1664    hidden_width: Pixels,
1665    total_char_count: usize,
1666    word_char_count: usize,
1667}
1668
1669impl JumpLabelFit {
1670    fn monospace(hide_end_offset: MultiBufferOffset) -> Self {
1671        Self {
1672            hide_end_offset,
1673            left_shift: px(0.0),
1674            scale_factor: 1.0,
1675        }
1676    }
1677}
1678
1679impl HiddenPrefixFitState {
1680    fn new(hide_end_offset: MultiBufferOffset) -> Self {
1681        Self {
1682            text: String::new(),
1683            hide_end_offset,
1684            hidden_width: px(0.0),
1685            total_char_count: 0,
1686            word_char_count: 0,
1687        }
1688    }
1689
1690    fn needs_more_width(&self, label_width: Pixels, max_left_shift: Pixels) -> bool {
1691        (self.hidden_width + max_left_shift) / label_width < HELIX_JUMP_MIN_LABEL_SCALE
1692    }
1693
1694    fn extend_to_fit<F: Fn(&str) -> Pixels>(
1695        &mut self,
1696        buffer: &MultiBufferSnapshot,
1697        range_start: MultiBufferOffset,
1698        range_end: MultiBufferOffset,
1699        word_end: MultiBufferOffset,
1700        label_width: Pixels,
1701        max_left_shift: Pixels,
1702        min_label_scale: f32,
1703        width_of: &F,
1704    ) {
1705        let mut offset = range_start;
1706        for chunk in buffer.text_for_range(range_start..range_end) {
1707            for (idx, ch) in chunk.char_indices() {
1708                let absolute = offset + idx;
1709
1710                self.total_char_count += 1;
1711                if self.total_char_count > HELIX_JUMP_MAX_HIDDEN_CHARS {
1712                    return;
1713                }
1714
1715                self.text.push(ch);
1716                let end_offset = absolute + ch.len_utf8();
1717
1718                if absolute < word_end && is_jump_word_char(ch) {
1719                    self.word_char_count += 1;
1720                }
1721
1722                if self.word_char_count < 2 {
1723                    continue;
1724                }
1725
1726                self.hide_end_offset = end_offset;
1727                self.hidden_width = width_of(self.text.as_str());
1728
1729                let effective_width = self.hidden_width + max_left_shift;
1730                let scale_needed = if label_width > px(0.0) {
1731                    (effective_width / label_width).min(1.0)
1732                } else {
1733                    1.0
1734                };
1735
1736                if scale_needed >= min_label_scale {
1737                    return;
1738                }
1739            }
1740            offset += chunk.len();
1741        }
1742    }
1743}
1744
1745#[derive(Default)]
1746struct HelixJumpUiData {
1747    labels: Vec<HelixJumpLabel>,
1748    overlays: Vec<NavigationTargetOverlay>,
1749}
1750
1751#[cfg(test)]
1752mod test {
1753    use futures::StreamExt;
1754    use std::{fmt::Write, time::Duration};
1755
1756    use editor::{HighlightKey, MultiBufferOffset};
1757    use gpui::{KeyBinding, UpdateGlobal, VisualTestContext};
1758    use indoc::indoc;
1759    use language::{CursorShape, Point};
1760    use project::FakeFs;
1761    use search::{ProjectSearchView, project_search};
1762    use serde_json::json;
1763    use settings::{SettingsStore, ThemeColorsContent, ThemeStyleContent};
1764    use theme::ActiveTheme as _;
1765    use util::path;
1766    use workspace::{DeploySearch, MultiWorkspace};
1767
1768    use super::{HELIX_JUMP_LABEL_LIMIT, HelixJumpToWord};
1769    use crate::{
1770        HELIX_JUMP_OVERLAY_KEY, Vim, VimAddon,
1771        state::{Mode, Operator},
1772        test::VimTestContext,
1773    };
1774
1775    fn active_helix_jump_labels(cx: &mut VimTestContext) -> Vec<(String, String)> {
1776        cx.update_editor(|editor, window, cx| {
1777            let labels = match editor
1778                .addon::<VimAddon>()
1779                .unwrap()
1780                .entity
1781                .read(cx)
1782                .operator_stack
1783                .last()
1784                .cloned()
1785            {
1786                Some(Operator::HelixJump { labels, .. }) => labels,
1787                other => panic!("expected active HelixJump operator, got {other:?}"),
1788            };
1789
1790            let snapshot = editor.snapshot(window, cx);
1791            let buffer_snapshot = snapshot.display_snapshot.buffer_snapshot();
1792
1793            labels
1794                .into_iter()
1795                .map(|label| {
1796                    let jump_label = label.label.iter().collect::<String>();
1797                    let word = buffer_snapshot
1798                        .text_for_range(label.range)
1799                        .collect::<String>();
1800                    (jump_label, word)
1801                })
1802                .collect()
1803        })
1804    }
1805
1806    fn helix_jump_label_for_word(cx: &mut VimTestContext, target_word: &str) -> String {
1807        active_helix_jump_labels(cx)
1808            .into_iter()
1809            .find_map(|(label, word)| (word == target_word).then_some(label))
1810            .unwrap_or_else(|| {
1811                let mut message = String::new();
1812                let labels = active_helix_jump_labels(cx);
1813                let _ = write!(
1814                    &mut message,
1815                    "expected jump label for word {target_word:?}, available labels: {labels:?}"
1816                );
1817                panic!("{message}");
1818            })
1819    }
1820
1821    fn jump_to_word(cx: &mut VimTestContext, target_word: &str) {
1822        jump_to_word_with_keystrokes(cx, "g w", target_word);
1823    }
1824
1825    fn jump_to_word_with_keystrokes(cx: &mut VimTestContext, keystrokes: &str, target_word: &str) {
1826        cx.simulate_keystrokes(keystrokes);
1827
1828        let label = helix_jump_label_for_word(cx, target_word);
1829
1830        let mut chars = label.chars();
1831        let first = chars.next().expect("jump labels are two characters long");
1832        let second = chars.next().expect("jump labels are two characters long");
1833        cx.simulate_keystrokes(&format!("{first} {second}"));
1834    }
1835
1836    fn bind_vim_jump_to_word(cx: &mut VimTestContext, keystrokes: &'static str) {
1837        cx.update(|_, cx| {
1838            cx.bind_keys([KeyBinding::new(
1839                keystrokes,
1840                HelixJumpToWord,
1841                Some("vim_mode == normal || vim_mode == visual"),
1842            )])
1843        });
1844    }
1845
1846    fn active_helix_jump_overlay_counts(cx: &mut VimTestContext) -> (usize, usize) {
1847        let covered_text_range_count = cx.update_editor(|editor, window, cx| {
1848            let snapshot = editor.snapshot(window, cx);
1849            snapshot
1850                .text_highlight_ranges(HighlightKey::NavigationOverlay(HELIX_JUMP_OVERLAY_KEY))
1851                .map(|ranges| ranges.as_ref().clone().1.len())
1852                .unwrap_or_default()
1853        });
1854        let label_count = match cx.active_operator() {
1855            Some(Operator::HelixJump { labels, .. }) => labels.len(),
1856            _ => 0,
1857        };
1858
1859        (covered_text_range_count, label_count)
1860    }
1861
1862    fn assert_helix_jump_cleared(cx: &mut VimTestContext, expected_overlay_counts: (usize, usize)) {
1863        assert_eq!(cx.active_operator(), None);
1864        assert_eq!(
1865            active_helix_jump_overlay_counts(cx),
1866            expected_overlay_counts,
1867            "expected Helix jump UI to be fully cleared"
1868        );
1869    }
1870
1871    fn helix_jump_labels_for_full_buffer(cx: &mut VimTestContext) -> Vec<(String, String)> {
1872        cx.update_editor(|editor, window, cx| {
1873            let snapshot = editor.snapshot(window, cx);
1874            let display_snapshot = &snapshot.display_snapshot;
1875            let buffer_snapshot = display_snapshot.buffer_snapshot();
1876            let selections = editor.selections.all::<Point>(display_snapshot);
1877            let skip_data = Vim::selection_skip_offsets(buffer_snapshot, &selections, false);
1878            let cursor_offset = selections
1879                .first()
1880                .map(|selection| buffer_snapshot.point_to_offset(selection.head()))
1881                .unwrap_or(MultiBufferOffset(0));
1882            let style = editor.style(cx);
1883            let font = style.text.font();
1884            let font_size = style.text.font_size.to_pixels(window.rem_size());
1885            let label_color = cx.theme().colors().vim_helix_jump_label_foreground;
1886            let data = Vim::build_helix_jump_ui_data(
1887                buffer_snapshot,
1888                MultiBufferOffset(0),
1889                buffer_snapshot.len(),
1890                cursor_offset,
1891                label_color,
1892                &skip_data,
1893                window.text_system(),
1894                font,
1895                font_size,
1896            );
1897
1898            data.labels
1899                .into_iter()
1900                .map(|label| {
1901                    let jump_label = label.label.iter().collect::<String>();
1902                    let word = buffer_snapshot
1903                        .text_for_range(label.range)
1904                        .collect::<String>();
1905                    (jump_label, word)
1906                })
1907                .collect()
1908        })
1909    }
1910
1911    #[gpui::test]
1912    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
1913        let mut cx = VimTestContext::new(cx, true).await;
1914        cx.enable_helix();
1915        // «
1916        // ˇ
1917        // »
1918        cx.set_state(
1919            indoc! {"
1920            Th«e quiˇ»ck brown
1921            fox jumps over
1922            the lazy dog."},
1923            Mode::HelixNormal,
1924        );
1925
1926        cx.simulate_keystrokes("w");
1927
1928        cx.assert_state(
1929            indoc! {"
1930            The qu«ick ˇ»brown
1931            fox jumps over
1932            the lazy dog."},
1933            Mode::HelixNormal,
1934        );
1935
1936        cx.simulate_keystrokes("w");
1937
1938        cx.assert_state(
1939            indoc! {"
1940            The quick «brownˇ»
1941            fox jumps over
1942            the lazy dog."},
1943            Mode::HelixNormal,
1944        );
1945
1946        cx.simulate_keystrokes("2 b");
1947
1948        cx.assert_state(
1949            indoc! {"
1950            The «ˇquick »brown
1951            fox jumps over
1952            the lazy dog."},
1953            Mode::HelixNormal,
1954        );
1955
1956        cx.simulate_keystrokes("down e up");
1957
1958        cx.assert_state(
1959            indoc! {"
1960            The quicˇk brown
1961            fox jumps over
1962            the lazy dog."},
1963            Mode::HelixNormal,
1964        );
1965
1966        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
1967
1968        cx.simulate_keystroke("b");
1969
1970        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
1971    }
1972
1973    #[gpui::test]
1974    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
1975        let mut cx = VimTestContext::new(cx, true).await;
1976        cx.enable_helix();
1977
1978        // Setup custom keybindings for subword motions so we can use the bindings
1979        // in `simulate_keystroke`.
1980        cx.update(|_window, cx| {
1981            cx.bind_keys([KeyBinding::new(
1982                "w",
1983                crate::motion::NextSubwordStart {
1984                    ignore_punctuation: false,
1985                },
1986                None,
1987            )]);
1988        });
1989
1990        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1991        cx.simulate_keystroke("w");
1992        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1993        cx.simulate_keystroke("w");
1994        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1995        cx.simulate_keystroke("w");
1996        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1997
1998        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1999        cx.simulate_keystroke("w");
2000        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
2001        cx.simulate_keystroke("w");
2002        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
2003        cx.simulate_keystroke("w");
2004        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
2005
2006        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
2007        cx.simulate_keystroke("w");
2008        cx.assert_state("«foo_ˇ»bar_baz", Mode::HelixNormal);
2009        cx.simulate_keystroke("w");
2010        cx.assert_state("foo_«bar_ˇ»baz", Mode::HelixNormal);
2011
2012        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
2013        cx.simulate_keystroke("w");
2014        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
2015        cx.simulate_keystroke("w");
2016        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
2017
2018        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
2019        cx.simulate_keystroke("w");
2020        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
2021        cx.simulate_keystroke("w");
2022        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
2023        cx.simulate_keystroke("w");
2024        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
2025
2026        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
2027        cx.simulate_keystroke("w");
2028        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
2029        cx.simulate_keystroke("w");
2030        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
2031        cx.simulate_keystroke("w");
2032        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
2033        cx.simulate_keystroke("w");
2034        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
2035        cx.simulate_keystroke("w");
2036        cx.assert_state("<?php\n\n$some«Variable ˇ»= 2;", Mode::HelixNormal);
2037        cx.simulate_keystroke("w");
2038        cx.assert_state("<?php\n\n$someVariable «= ˇ»2;", Mode::HelixNormal);
2039        cx.simulate_keystroke("w");
2040        cx.assert_state("<?php\n\n$someVariable = «2ˇ»;", Mode::HelixNormal);
2041        cx.simulate_keystroke("w");
2042        cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
2043    }
2044
2045    #[gpui::test]
2046    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
2047        let mut cx = VimTestContext::new(cx, true).await;
2048        cx.enable_helix();
2049
2050        // Setup custom keybindings for subword motions so we can use the bindings
2051        // in `simulate_keystroke`.
2052        cx.update(|_window, cx| {
2053            cx.bind_keys([KeyBinding::new(
2054                "e",
2055                crate::motion::NextSubwordEnd {
2056                    ignore_punctuation: false,
2057                },
2058                None,
2059            )]);
2060        });
2061
2062        cx.set_state("ˇfoo.bar", Mode::HelixNormal);
2063        cx.simulate_keystroke("e");
2064        cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
2065        cx.simulate_keystroke("e");
2066        cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
2067        cx.simulate_keystroke("e");
2068        cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
2069
2070        cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
2071        cx.simulate_keystroke("e");
2072        cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
2073        cx.simulate_keystroke("e");
2074        cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
2075        cx.simulate_keystroke("e");
2076        cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
2077
2078        cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
2079        cx.simulate_keystroke("e");
2080        cx.assert_state("«fooˇ»_bar_baz", Mode::HelixNormal);
2081        cx.simulate_keystroke("e");
2082        cx.assert_state("foo«_barˇ»_baz", Mode::HelixNormal);
2083        cx.simulate_keystroke("e");
2084        cx.assert_state("foo_bar«_bazˇ»", Mode::HelixNormal);
2085
2086        cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
2087        cx.simulate_keystroke("e");
2088        cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
2089        cx.simulate_keystroke("e");
2090        cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
2091        cx.simulate_keystroke("e");
2092        cx.assert_state("fooBar«Bazˇ»", Mode::HelixNormal);
2093
2094        cx.set_state("ˇfoo;bar", Mode::HelixNormal);
2095        cx.simulate_keystroke("e");
2096        cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
2097        cx.simulate_keystroke("e");
2098        cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
2099        cx.simulate_keystroke("e");
2100        cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
2101
2102        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
2103        cx.simulate_keystroke("e");
2104        cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
2105        cx.simulate_keystroke("e");
2106        cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
2107        cx.simulate_keystroke("e");
2108        cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
2109        cx.simulate_keystroke("e");
2110        cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
2111        cx.simulate_keystroke("e");
2112        cx.assert_state("<?php\n\n$some«Variableˇ» = 2;", Mode::HelixNormal);
2113        cx.simulate_keystroke("e");
2114        cx.assert_state("<?php\n\n$someVariable« =ˇ» 2;", Mode::HelixNormal);
2115        cx.simulate_keystroke("e");
2116        cx.assert_state("<?php\n\n$someVariable =« 2ˇ»;", Mode::HelixNormal);
2117        cx.simulate_keystroke("e");
2118        cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
2119    }
2120
2121    #[gpui::test]
2122    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
2123        let mut cx = VimTestContext::new(cx, true).await;
2124        cx.enable_helix();
2125
2126        // Setup custom keybindings for subword motions so we can use the bindings
2127        // in `simulate_keystroke`.
2128        cx.update(|_window, cx| {
2129            cx.bind_keys([KeyBinding::new(
2130                "b",
2131                crate::motion::PreviousSubwordStart {
2132                    ignore_punctuation: false,
2133                },
2134                None,
2135            )]);
2136        });
2137
2138        cx.set_state("foo.barˇ", Mode::HelixNormal);
2139        cx.simulate_keystroke("b");
2140        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
2141        cx.simulate_keystroke("b");
2142        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
2143        cx.simulate_keystroke("b");
2144        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
2145
2146        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
2147        cx.simulate_keystroke("b");
2148        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
2149        cx.simulate_keystroke("b");
2150        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
2151        cx.simulate_keystroke("b");
2152        cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
2153        cx.simulate_keystroke("b");
2154        cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
2155
2156        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
2157        cx.simulate_keystroke("b");
2158        cx.assert_state("foo_bar_«ˇbaz»", Mode::HelixNormal);
2159        cx.simulate_keystroke("b");
2160        cx.assert_state("foo_«ˇbar_»baz", Mode::HelixNormal);
2161        cx.simulate_keystroke("b");
2162        cx.assert_state("«ˇfoo_»bar_baz", Mode::HelixNormal);
2163
2164        cx.set_state("foo;barˇ", Mode::HelixNormal);
2165        cx.simulate_keystroke("b");
2166        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
2167        cx.simulate_keystroke("b");
2168        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
2169        cx.simulate_keystroke("b");
2170        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
2171
2172        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
2173        cx.simulate_keystroke("b");
2174        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
2175        cx.simulate_keystroke("b");
2176        cx.assert_state("<?php\n\n$someVariable = «ˇ2»;", Mode::HelixNormal);
2177        cx.simulate_keystroke("b");
2178        cx.assert_state("<?php\n\n$someVariable «ˇ= »2;", Mode::HelixNormal);
2179        cx.simulate_keystroke("b");
2180        cx.assert_state("<?php\n\n$some«ˇVariable »= 2;", Mode::HelixNormal);
2181        cx.simulate_keystroke("b");
2182        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
2183        cx.simulate_keystroke("b");
2184        cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
2185        cx.simulate_keystroke("b");
2186        cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
2187        cx.simulate_keystroke("b");
2188        cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
2189
2190        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
2191        cx.simulate_keystroke("b");
2192        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
2193        cx.simulate_keystroke("b");
2194        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
2195        cx.simulate_keystroke("b");
2196        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
2197    }
2198
2199    #[gpui::test]
2200    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
2201        let mut cx = VimTestContext::new(cx, true).await;
2202        cx.enable_helix();
2203
2204        // Setup custom keybindings for subword motions so we can use the bindings
2205        // in `simulate_keystrokes`.
2206        cx.update(|_window, cx| {
2207            cx.bind_keys([KeyBinding::new(
2208                "g e",
2209                crate::motion::PreviousSubwordEnd {
2210                    ignore_punctuation: false,
2211                },
2212                None,
2213            )]);
2214        });
2215
2216        cx.set_state("foo.barˇ", Mode::HelixNormal);
2217        cx.simulate_keystrokes("g e");
2218        cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
2219        cx.simulate_keystrokes("g e");
2220        cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
2221        cx.simulate_keystrokes("g e");
2222        cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
2223
2224        cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
2225        cx.simulate_keystrokes("g e");
2226        cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
2227        cx.simulate_keystrokes("g e");
2228        cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
2229        cx.simulate_keystrokes("g e");
2230        cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
2231        cx.simulate_keystrokes("g e");
2232        cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
2233
2234        cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
2235        cx.simulate_keystrokes("g e");
2236        cx.assert_state("foo_bar«ˇ_baz»", Mode::HelixNormal);
2237        cx.simulate_keystrokes("g e");
2238        cx.assert_state("foo«ˇ_bar»_baz", Mode::HelixNormal);
2239        cx.simulate_keystrokes("g e");
2240        cx.assert_state("«ˇfoo»_bar_baz", Mode::HelixNormal);
2241
2242        cx.set_state("foo;barˇ", Mode::HelixNormal);
2243        cx.simulate_keystrokes("g e");
2244        cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
2245        cx.simulate_keystrokes("g e");
2246        cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
2247        cx.simulate_keystrokes("g e");
2248        cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
2249
2250        cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
2251        cx.simulate_keystrokes("g e");
2252        cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
2253        cx.simulate_keystrokes("g e");
2254        cx.assert_state("<?php\n\n$someVariable =«ˇ 2»;", Mode::HelixNormal);
2255        cx.simulate_keystrokes("g e");
2256        cx.assert_state("<?php\n\n$someVariable«ˇ =» 2;", Mode::HelixNormal);
2257        cx.simulate_keystrokes("g e");
2258        cx.assert_state("<?php\n\n$some«ˇVariable» = 2;", Mode::HelixNormal);
2259        cx.simulate_keystrokes("g e");
2260        cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
2261        cx.simulate_keystrokes("g e");
2262        cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
2263        cx.simulate_keystrokes("g e");
2264        cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
2265        cx.simulate_keystrokes("g e");
2266        cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
2267
2268        cx.set_state("fooBarBazˇ", Mode::HelixNormal);
2269        cx.simulate_keystrokes("g e");
2270        cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
2271        cx.simulate_keystrokes("g e");
2272        cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
2273        cx.simulate_keystrokes("g e");
2274        cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
2275    }
2276
2277    #[gpui::test]
2278    async fn test_delete(cx: &mut gpui::TestAppContext) {
2279        let mut cx = VimTestContext::new(cx, true).await;
2280        cx.enable_helix();
2281
2282        // test delete a selection
2283        cx.set_state(
2284            indoc! {"
2285            The qu«ick ˇ»brown
2286            fox jumps over
2287            the lazy dog."},
2288            Mode::HelixNormal,
2289        );
2290
2291        cx.simulate_keystrokes("d");
2292
2293        cx.assert_state(
2294            indoc! {"
2295            The quˇbrown
2296            fox jumps over
2297            the lazy dog."},
2298            Mode::HelixNormal,
2299        );
2300
2301        // test deleting a single character
2302        cx.simulate_keystrokes("d");
2303
2304        cx.assert_state(
2305            indoc! {"
2306            The quˇrown
2307            fox jumps over
2308            the lazy dog."},
2309            Mode::HelixNormal,
2310        );
2311    }
2312
2313    #[gpui::test]
2314    async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
2315        let mut cx = VimTestContext::new(cx, true).await;
2316
2317        cx.set_state(
2318            indoc! {"
2319            The quick brownˇ
2320            fox jumps over
2321            the lazy dog."},
2322            Mode::HelixNormal,
2323        );
2324
2325        cx.simulate_keystrokes("d");
2326
2327        cx.assert_state(
2328            indoc! {"
2329            The quick brownˇfox jumps over
2330            the lazy dog."},
2331            Mode::HelixNormal,
2332        );
2333    }
2334
2335    // Deleting a selection that ends at the last non-newline character should
2336    // leave the cursor on the newline (matching Helix), not clamp it onto the
2337    // character to the left of the selection.
2338    #[gpui::test]
2339    async fn test_delete_to_end_of_line_keeps_cursor_on_newline(cx: &mut gpui::TestAppContext) {
2340        let mut cx = VimTestContext::new(cx, true).await;
2341        cx.enable_helix();
2342
2343        cx.set_state(
2344            indoc! {"
2345            ab«cdefgˇ»
2346            hij"},
2347            Mode::HelixNormal,
2348        );
2349
2350        cx.simulate_keystrokes("d");
2351
2352        cx.assert_state(
2353            indoc! {"
2354            abˇ
2355            hij"},
2356            Mode::HelixNormal,
2357        );
2358    }
2359
2360    // #[gpui::test]
2361    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
2362    //     let mut cx = VimTestContext::new(cx, true).await;
2363
2364    //     cx.set_state(
2365    //         indoc! {"
2366    //         The quick brown
2367    //         fox jumps over
2368    //         the lazy dog.ˇ"},
2369    //         Mode::HelixNormal,
2370    //     );
2371
2372    //     cx.simulate_keystrokes("d");
2373
2374    //     cx.assert_state(
2375    //         indoc! {"
2376    //         The quick brown
2377    //         fox jumps over
2378    //         the lazy dog.ˇ"},
2379    //         Mode::HelixNormal,
2380    //     );
2381    // }
2382
2383    #[gpui::test]
2384    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
2385        let mut cx = VimTestContext::new(cx, true).await;
2386        cx.enable_helix();
2387
2388        cx.set_state(
2389            indoc! {"
2390            The quˇick brown
2391            fox jumps over
2392            the lazy dog."},
2393            Mode::HelixNormal,
2394        );
2395
2396        cx.simulate_keystrokes("f z");
2397
2398        cx.assert_state(
2399            indoc! {"
2400                The qu«ick brown
2401                fox jumps over
2402                the lazˇ»y dog."},
2403            Mode::HelixNormal,
2404        );
2405
2406        cx.simulate_keystrokes("F e F e");
2407
2408        cx.assert_state(
2409            indoc! {"
2410                The quick brown
2411                fox jumps ov«ˇer
2412                the» lazy dog."},
2413            Mode::HelixNormal,
2414        );
2415
2416        cx.simulate_keystrokes("e 2 F e");
2417
2418        cx.assert_state(
2419            indoc! {"
2420                Th«ˇe quick brown
2421                fox jumps over»
2422                the lazy dog."},
2423            Mode::HelixNormal,
2424        );
2425
2426        cx.simulate_keystrokes("t r t r");
2427
2428        cx.assert_state(
2429            indoc! {"
2430                The quick «brown
2431                fox jumps oveˇ»r
2432                the lazy dog."},
2433            Mode::HelixNormal,
2434        );
2435    }
2436
2437    #[gpui::test]
2438    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
2439        let mut cx = VimTestContext::new(cx, true).await;
2440        cx.enable_helix();
2441
2442        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
2443
2444        cx.simulate_keystroke("w");
2445
2446        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
2447
2448        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
2449
2450        cx.simulate_keystroke("b");
2451
2452        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
2453    }
2454
2455    #[gpui::test]
2456    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
2457        let mut cx = VimTestContext::new(cx, true).await;
2458        cx.enable_helix();
2459        cx.set_state(
2460            indoc! {"
2461            «The ˇ»quick brown
2462            fox jumps over
2463            the lazy dog."},
2464            Mode::HelixNormal,
2465        );
2466
2467        cx.simulate_keystrokes("i");
2468
2469        cx.assert_state(
2470            indoc! {"
2471            ˇThe quick brown
2472            fox jumps over
2473            the lazy dog."},
2474            Mode::Insert,
2475        );
2476    }
2477
2478    #[gpui::test]
2479    async fn test_append(cx: &mut gpui::TestAppContext) {
2480        let mut cx = VimTestContext::new(cx, true).await;
2481        cx.enable_helix();
2482        // test from the end of the selection
2483        cx.set_state(
2484            indoc! {"
2485            «Theˇ» quick brown
2486            fox jumps over
2487            the lazy dog."},
2488            Mode::HelixNormal,
2489        );
2490
2491        cx.simulate_keystrokes("a");
2492
2493        cx.assert_state(
2494            indoc! {"
2495            Theˇ quick brown
2496            fox jumps over
2497            the lazy dog."},
2498            Mode::Insert,
2499        );
2500
2501        // test from the beginning of the selection
2502        cx.set_state(
2503            indoc! {"
2504            «ˇThe» quick brown
2505            fox jumps over
2506            the lazy dog."},
2507            Mode::HelixNormal,
2508        );
2509
2510        cx.simulate_keystrokes("a");
2511
2512        cx.assert_state(
2513            indoc! {"
2514            Theˇ quick brown
2515            fox jumps over
2516            the lazy dog."},
2517            Mode::Insert,
2518        );
2519    }
2520
2521    #[gpui::test]
2522    async fn test_replace(cx: &mut gpui::TestAppContext) {
2523        let mut cx = VimTestContext::new(cx, true).await;
2524        cx.enable_helix();
2525
2526        // No selection (single character)
2527        cx.set_state("ˇaa", Mode::HelixNormal);
2528
2529        cx.simulate_keystrokes("r x");
2530
2531        cx.assert_state("ˇxa", Mode::HelixNormal);
2532
2533        // Cursor at the beginning
2534        cx.set_state("«ˇaa»", Mode::HelixNormal);
2535
2536        cx.simulate_keystrokes("r x");
2537
2538        cx.assert_state("«ˇxx»", Mode::HelixNormal);
2539
2540        // Cursor at the end
2541        cx.set_state("«aaˇ»", Mode::HelixNormal);
2542
2543        cx.simulate_keystrokes("r x");
2544
2545        cx.assert_state("«xxˇ»", Mode::HelixNormal);
2546
2547        cx.set_state("«aaˇ»", Mode::HelixSelect);
2548
2549        cx.simulate_keystrokes("r x");
2550
2551        cx.assert_state("«xxˇ»", Mode::HelixNormal);
2552    }
2553
2554    #[gpui::test]
2555    async fn test_replace_with_crlf(cx: &mut gpui::TestAppContext) {
2556        let mut cx = VimTestContext::new(cx, true).await;
2557        cx.enable_helix();
2558        cx.set_state("«xˇ»z", Mode::HelixNormal);
2559
2560        let vim =
2561            cx.update_editor(|editor, _window, _cx| editor.addon::<VimAddon>().cloned().unwrap());
2562        cx.update(|window, cx| {
2563            vim.entity.update(cx, |vim, cx| {
2564                vim.helix_replace("a\r\nb", window, cx);
2565            });
2566        });
2567
2568        cx.assert_state("«a\nbˇ»z", Mode::HelixNormal);
2569    }
2570
2571    #[gpui::test]
2572    async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
2573        let mut cx = VimTestContext::new(cx, true).await;
2574        cx.enable_helix();
2575
2576        // Test yanking current character with no selection
2577        cx.set_state("hello ˇworld", Mode::HelixNormal);
2578        cx.simulate_keystrokes("y");
2579
2580        // Test cursor remains at the same position after yanking single character
2581        cx.assert_state("hello ˇworld", Mode::HelixNormal);
2582        cx.shared_clipboard().assert_eq("w");
2583
2584        // Move cursor and yank another character
2585        cx.simulate_keystrokes("l");
2586        cx.simulate_keystrokes("y");
2587        cx.shared_clipboard().assert_eq("o");
2588
2589        // Test yanking with existing selection
2590        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
2591        cx.simulate_keystrokes("y");
2592        cx.shared_clipboard().assert_eq("worl");
2593        cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
2594
2595        // Test yanking in select mode character by character
2596        cx.set_state("hello ˇworld", Mode::HelixNormal);
2597        cx.simulate_keystroke("v");
2598        cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
2599        cx.simulate_keystroke("y");
2600        cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
2601        cx.shared_clipboard().assert_eq("w");
2602    }
2603
2604    #[gpui::test]
2605    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
2606        let mut cx = VimTestContext::new(cx, true).await;
2607        cx.enable_helix();
2608
2609        // First copy some text to clipboard
2610        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
2611        cx.simulate_keystrokes("y");
2612
2613        // Test paste with shift-r on single cursor
2614        cx.set_state("foo ˇbar", Mode::HelixNormal);
2615        cx.simulate_keystrokes("shift-r");
2616
2617        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
2618
2619        // Test paste with shift-r on selection
2620        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
2621        cx.simulate_keystrokes("shift-r");
2622
2623        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
2624    }
2625
2626    #[gpui::test]
2627    async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
2628        let mut cx = VimTestContext::new(cx, true).await;
2629
2630        assert_eq!(cx.mode(), Mode::Normal);
2631        cx.enable_helix();
2632
2633        cx.simulate_keystrokes("v");
2634        assert_eq!(cx.mode(), Mode::HelixSelect);
2635        cx.simulate_keystrokes("escape");
2636        assert_eq!(cx.mode(), Mode::HelixNormal);
2637    }
2638
2639    #[gpui::test]
2640    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
2641        let mut cx = VimTestContext::new(cx, true).await;
2642        cx.enable_helix();
2643
2644        // Make a modification at a specific location
2645        cx.set_state("ˇhello", Mode::HelixNormal);
2646        assert_eq!(cx.mode(), Mode::HelixNormal);
2647        cx.simulate_keystrokes("i");
2648        assert_eq!(cx.mode(), Mode::Insert);
2649        cx.simulate_keystrokes("escape");
2650        assert_eq!(cx.mode(), Mode::HelixNormal);
2651    }
2652
2653    #[gpui::test]
2654    async fn test_helix_select_append(cx: &mut gpui::TestAppContext) {
2655        let mut cx = VimTestContext::new(cx, true).await;
2656        cx.enable_helix();
2657
2658        cx.set_state("aˇbcd", Mode::HelixNormal);
2659        cx.simulate_keystrokes("v a");
2660        cx.assert_state("abˇcd", Mode::Insert);
2661    }
2662
2663    #[gpui::test]
2664    async fn test_helix_select_move_to_trailing_newline(cx: &mut gpui::TestAppContext) {
2665        let mut cx = VimTestContext::new(cx, true).await;
2666        cx.enable_helix();
2667
2668        cx.set_state("line one\nline two\nline threˇe\n", Mode::HelixNormal);
2669        cx.simulate_keystrokes("v");
2670        cx.assert_state("line one\nline two\nline thre«eˇ»\n", Mode::HelixSelect);
2671        cx.simulate_keystrokes("l");
2672        cx.assert_state("line one\nline two\nline thre«e\nˇ»", Mode::HelixSelect);
2673    }
2674
2675    #[gpui::test]
2676    async fn test_helix_select_trailing_newline(cx: &mut gpui::TestAppContext) {
2677        let mut cx = VimTestContext::new(cx, true).await;
2678        cx.enable_helix();
2679
2680        cx.set_state("line one\nline two\nline threeˇ\n", Mode::HelixNormal);
2681        cx.simulate_keystrokes("v");
2682        cx.assert_state("line one\nline two\nline three«\nˇ»", Mode::HelixSelect);
2683        cx.simulate_keystrokes("g h");
2684        cx.assert_state("line one\nline two\n«ˇline three\n»", Mode::HelixSelect);
2685    }
2686
2687    #[gpui::test]
2688    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
2689        let mut cx = VimTestContext::new(cx, true).await;
2690        cx.enable_helix();
2691
2692        // Make a modification at a specific location
2693        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
2694        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
2695        cx.simulate_keystrokes("i");
2696        cx.simulate_keystrokes("escape");
2697        cx.simulate_keystrokes("i");
2698        cx.simulate_keystrokes("m o d i f i e d space");
2699        cx.simulate_keystrokes("escape");
2700
2701        // TODO: this fails, because state is no longer helix
2702        cx.assert_state(
2703            "line one\nline modified ˇtwo\nline three",
2704            Mode::HelixNormal,
2705        );
2706
2707        // Move cursor away from the modification
2708        cx.simulate_keystrokes("up");
2709
2710        // Use "g ." to go back to last modification
2711        cx.simulate_keystrokes("g .");
2712
2713        // Verify we're back at the modification location and still in HelixNormal mode
2714        cx.assert_state(
2715            "line one\nline modifiedˇ two\nline three",
2716            Mode::HelixNormal,
2717        );
2718    }
2719
2720    #[gpui::test]
2721    async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
2722        let mut cx = VimTestContext::new(cx, true).await;
2723        cx.set_state(
2724            "line one\nline ˇtwo\nline three\nline four",
2725            Mode::HelixNormal,
2726        );
2727        cx.simulate_keystrokes("2 x");
2728        cx.assert_state(
2729            "line one\n«line two\nline three\nˇ»line four",
2730            Mode::HelixNormal,
2731        );
2732
2733        // Test extending existing line selection
2734        cx.set_state(
2735            indoc! {"
2736            li«ˇne one
2737            li»ne two
2738            line three
2739            line four"},
2740            Mode::HelixNormal,
2741        );
2742        cx.simulate_keystrokes("x");
2743        cx.assert_state(
2744            indoc! {"
2745            «line one
2746            line two
2747            ˇ»line three
2748            line four"},
2749            Mode::HelixNormal,
2750        );
2751
2752        // Pressing x in empty line, select next line (because helix considers cursor a selection)
2753        cx.set_state(
2754            indoc! {"
2755            line one
2756            ˇ
2757            line three
2758            line four
2759            line five
2760            line six"},
2761            Mode::HelixNormal,
2762        );
2763        cx.simulate_keystrokes("x");
2764        cx.assert_state(
2765            indoc! {"
2766            line one
2767            «
2768            line three
2769            ˇ»line four
2770            line five
2771            line six"},
2772            Mode::HelixNormal,
2773        );
2774
2775        // Another x should only select the next line
2776        cx.simulate_keystrokes("x");
2777        cx.assert_state(
2778            indoc! {"
2779            line one
2780            «
2781            line three
2782            line four
2783            ˇ»line five
2784            line six"},
2785            Mode::HelixNormal,
2786        );
2787
2788        // Empty line with count selects extra + count lines
2789        cx.set_state(
2790            indoc! {"
2791            line one
2792            ˇ
2793            line three
2794            line four
2795            line five"},
2796            Mode::HelixNormal,
2797        );
2798        cx.simulate_keystrokes("2 x");
2799        cx.assert_state(
2800            indoc! {"
2801            line one
2802            «
2803            line three
2804            line four
2805            ˇ»line five"},
2806            Mode::HelixNormal,
2807        );
2808
2809        // Compare empty vs non-empty line behavior
2810        cx.set_state(
2811            indoc! {"
2812            ˇnon-empty line
2813            line two
2814            line three"},
2815            Mode::HelixNormal,
2816        );
2817        cx.simulate_keystrokes("x");
2818        cx.assert_state(
2819            indoc! {"
2820            «non-empty line
2821            ˇ»line two
2822            line three"},
2823            Mode::HelixNormal,
2824        );
2825
2826        // Same test but with empty line - should select one extra
2827        cx.set_state(
2828            indoc! {"
2829            ˇ
2830            line two
2831            line three"},
2832            Mode::HelixNormal,
2833        );
2834        cx.simulate_keystrokes("x");
2835        cx.assert_state(
2836            indoc! {"
2837            «
2838            line two
2839            ˇ»line three"},
2840            Mode::HelixNormal,
2841        );
2842
2843        // Test selecting multiple lines with count
2844        cx.set_state(
2845            indoc! {"
2846            ˇline one
2847            line two
2848            line threeˇ
2849            line four
2850            line five"},
2851            Mode::HelixNormal,
2852        );
2853        cx.simulate_keystrokes("x");
2854        cx.assert_state(
2855            indoc! {"
2856            «line one
2857            ˇ»line two
2858            «line three
2859            ˇ»line four
2860            line five"},
2861            Mode::HelixNormal,
2862        );
2863        cx.simulate_keystrokes("x");
2864        // Adjacent line selections stay separate (not merged)
2865        cx.assert_state(
2866            indoc! {"
2867            «line one
2868            line two
2869            ˇ»«line three
2870            line four
2871            ˇ»line five"},
2872            Mode::HelixNormal,
2873        );
2874
2875        // Test selecting with an empty line below the current line
2876        cx.set_state(
2877            indoc! {"
2878            line one
2879            line twoˇ
2880
2881            line four
2882            line five"},
2883            Mode::HelixNormal,
2884        );
2885        cx.simulate_keystrokes("x");
2886        cx.assert_state(
2887            indoc! {"
2888            line one
2889            «line two
2890            ˇ»
2891            line four
2892            line five"},
2893            Mode::HelixNormal,
2894        );
2895        cx.simulate_keystrokes("x");
2896        cx.assert_state(
2897            indoc! {"
2898            line one
2899            «line two
2900
2901            ˇ»line four
2902            line five"},
2903            Mode::HelixNormal,
2904        );
2905        cx.simulate_keystrokes("x");
2906        cx.assert_state(
2907            indoc! {"
2908            line one
2909            «line two
2910
2911            line four
2912            ˇ»line five"},
2913            Mode::HelixNormal,
2914        );
2915
2916        cx.set_state("oneˇ\ntwo\nthree", Mode::HelixNormal);
2917        cx.simulate_keystrokes("d u x");
2918        cx.assert_state("«one\nˇ»two\nthree", Mode::HelixNormal);
2919    }
2920
2921    #[gpui::test]
2922    async fn test_helix_insert_before_after_select_lines(cx: &mut gpui::TestAppContext) {
2923        let mut cx = VimTestContext::new(cx, true).await;
2924
2925        cx.set_state(
2926            "line one\nline ˇtwo\nline three\nline four",
2927            Mode::HelixNormal,
2928        );
2929        cx.simulate_keystrokes("2 x");
2930        cx.assert_state(
2931            "line one\n«line two\nline three\nˇ»line four",
2932            Mode::HelixNormal,
2933        );
2934        cx.simulate_keystrokes("o");
2935        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
2936
2937        cx.set_state(
2938            "line one\nline ˇtwo\nline three\nline four",
2939            Mode::HelixNormal,
2940        );
2941        cx.simulate_keystrokes("2 x");
2942        cx.assert_state(
2943            "line one\n«line two\nline three\nˇ»line four",
2944            Mode::HelixNormal,
2945        );
2946        cx.simulate_keystrokes("shift-o");
2947        cx.assert_state("line one\nˇ\nline two\nline three\nline four", Mode::Insert);
2948    }
2949
2950    #[gpui::test]
2951    async fn test_helix_insert_before_after_helix_select(cx: &mut gpui::TestAppContext) {
2952        let mut cx = VimTestContext::new(cx, true).await;
2953        cx.enable_helix();
2954
2955        // Test new line in selection direction
2956        cx.set_state(
2957            "ˇline one\nline two\nline three\nline four",
2958            Mode::HelixNormal,
2959        );
2960        cx.simulate_keystrokes("v j j");
2961        cx.assert_state(
2962            "«line one\nline two\nlˇ»ine three\nline four",
2963            Mode::HelixSelect,
2964        );
2965        cx.simulate_keystrokes("o");
2966        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
2967
2968        cx.set_state(
2969            "line one\nline two\nˇline three\nline four",
2970            Mode::HelixNormal,
2971        );
2972        cx.simulate_keystrokes("v k k");
2973        cx.assert_state(
2974            "«ˇline one\nline two\nl»ine three\nline four",
2975            Mode::HelixSelect,
2976        );
2977        cx.simulate_keystrokes("shift-o");
2978        cx.assert_state(\nline one\nline two\nline three\nline four", Mode::Insert);
2979
2980        // Test new line in opposite selection direction
2981        cx.set_state(
2982            "ˇline one\nline two\nline three\nline four",
2983            Mode::HelixNormal,
2984        );
2985        cx.simulate_keystrokes("v j j");
2986        cx.assert_state(
2987            "«line one\nline two\nlˇ»ine three\nline four",
2988            Mode::HelixSelect,
2989        );
2990        cx.simulate_keystrokes("shift-o");
2991        cx.assert_state(\nline one\nline two\nline three\nline four", Mode::Insert);
2992
2993        cx.set_state(
2994            "line one\nline two\nˇline three\nline four",
2995            Mode::HelixNormal,
2996        );
2997        cx.simulate_keystrokes("v k k");
2998        cx.assert_state(
2999            "«ˇline one\nline two\nl»ine three\nline four",
3000            Mode::HelixSelect,
3001        );
3002        cx.simulate_keystrokes("o");
3003        cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
3004    }
3005
3006    #[gpui::test]
3007    async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
3008        let mut cx = VimTestContext::new(cx, true).await;
3009
3010        assert_eq!(cx.mode(), Mode::Normal);
3011        cx.enable_helix();
3012
3013        cx.set_state("ˇhello", Mode::HelixNormal);
3014        cx.simulate_keystrokes("l v l l");
3015        cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
3016    }
3017
3018    #[gpui::test]
3019    async fn test_helix_select_end_of_line(cx: &mut gpui::TestAppContext) {
3020        let mut cx = VimTestContext::new(cx, true).await;
3021        cx.enable_helix();
3022
3023        // v g l d should delete to end of line without consuming the newline
3024        cx.set_state("ˇThe quick brown\nfox jumps over", Mode::HelixNormal);
3025        cx.simulate_keystrokes("v g l d");
3026        cx.assert_state(\nfox jumps over", Mode::HelixNormal);
3027
3028        // same from the middle of a line — the cursor rests on the trailing
3029        // newline, matching Helix and the whole-line case above.
3030        cx.set_state("The ˇquick brown\nfox jumps over", Mode::HelixNormal);
3031        cx.simulate_keystrokes("v g l d");
3032        cx.assert_state("The ˇ\nfox jumps over", Mode::HelixNormal);
3033    }
3034
3035    #[gpui::test]
3036    async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
3037        let mut cx = VimTestContext::new(cx, true).await;
3038
3039        assert_eq!(cx.mode(), Mode::Normal);
3040        cx.enable_helix();
3041
3042        // Start with multiple cursors (no selections)
3043        cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
3044
3045        // Enter select mode and move right twice
3046        cx.simulate_keystrokes("v l l");
3047
3048        // Each cursor should independently create and extend its own selection
3049        cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
3050    }
3051
3052    #[gpui::test]
3053    async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
3054        let mut cx = VimTestContext::new(cx, true).await;
3055
3056        cx.set_state("ˇone two", Mode::Normal);
3057        cx.simulate_keystrokes("v w");
3058        cx.assert_state("«one tˇ»wo", Mode::Visual);
3059
3060        // In Vim, this selects "t". In helix selections stops just before "t"
3061
3062        cx.enable_helix();
3063        cx.set_state("ˇone two", Mode::HelixNormal);
3064        cx.simulate_keystrokes("v w");
3065        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
3066    }
3067
3068    #[gpui::test]
3069    async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
3070        let mut cx = VimTestContext::new(cx, true).await;
3071
3072        cx.set_state("ˇone two", Mode::Normal);
3073        cx.simulate_keystrokes("v w");
3074        cx.assert_state("«one tˇ»wo", Mode::Visual);
3075        cx.simulate_keystrokes("escape");
3076        cx.assert_state("one ˇtwo", Mode::Normal);
3077
3078        cx.enable_helix();
3079        cx.set_state("ˇone two", Mode::HelixNormal);
3080        cx.simulate_keystrokes("v w");
3081        cx.assert_state("«one ˇ»two", Mode::HelixSelect);
3082        cx.simulate_keystrokes("escape");
3083        cx.assert_state("«one ˇ»two", Mode::HelixNormal);
3084    }
3085
3086    #[gpui::test]
3087    async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
3088        let mut cx = VimTestContext::new(cx, true).await;
3089        cx.enable_helix();
3090
3091        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
3092        cx.simulate_keystrokes("w");
3093        cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
3094
3095        cx.set_state("«ˇ»one two three", Mode::HelixSelect);
3096        cx.simulate_keystrokes("e");
3097        cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
3098    }
3099
3100    #[gpui::test]
3101    async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
3102        let mut cx = VimTestContext::new(cx, true).await;
3103        cx.enable_helix();
3104
3105        cx.set_state("ˇone two three", Mode::HelixNormal);
3106        cx.simulate_keystrokes("l l v h h h");
3107        cx.assert_state("«ˇone» two three", Mode::HelixSelect);
3108    }
3109
3110    // Regression test for ZED-758: helix motions called
3111    // `Editor::text_layout_details` on an editor whose `style` had never
3112    // been set, panicking on `unwrap()`.
3113    #[gpui::test]
3114    async fn test_helix_motion_on_unrendered_editor(cx: &mut gpui::TestAppContext) {
3115        use editor::{Editor, EditorMode, SelectionEffects};
3116        use multi_buffer::{MultiBuffer, MultiBufferOffset};
3117
3118        VimTestContext::init(cx);
3119        cx.update(|cx| {
3120            VimTestContext::init_keybindings(true, cx);
3121            SettingsStore::update_global(cx, |store, cx| {
3122                store.update_user_settings(cx, |s| {
3123                    s.vim_mode = Some(true);
3124                    s.helix_mode = Some(true);
3125                });
3126            });
3127        });
3128
3129        let cx = cx.add_empty_window();
3130
3131        let editor = cx.update(|window, cx| {
3132            use gpui::AppContext as _;
3133            let buffer = MultiBuffer::build_simple("one two three", cx);
3134            cx.new(|cx| {
3135                let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
3136                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3137                    s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
3138                });
3139                editor
3140            })
3141        });
3142
3143        let vim = editor
3144            .read_with(cx, |editor, _| editor.addon::<VimAddon>().cloned())
3145            .expect("VimAddon should be auto-attached to new editors when vim mode is enabled");
3146
3147        cx.update(|window, cx| {
3148            vim.entity.update(cx, |vim, cx| {
3149                vim.switch_mode(Mode::HelixNormal, true, window, cx);
3150                vim.helix_move_and_collapse(crate::motion::Motion::Left, None, window, cx);
3151            });
3152        });
3153
3154        let cursor_offset = cx.update(|_, cx| {
3155            editor.update(cx, |editor, cx| {
3156                editor
3157                    .selections
3158                    .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
3159                    .head()
3160            })
3161        });
3162        assert_eq!(cursor_offset, MultiBufferOffset(3));
3163    }
3164
3165    #[gpui::test]
3166    async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
3167        let mut cx = VimTestContext::new(cx, true).await;
3168        cx.enable_helix();
3169
3170        cx.set_state("ˇone two one", Mode::HelixNormal);
3171        cx.simulate_keystrokes("x");
3172        cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
3173        cx.simulate_keystrokes("s o n e");
3174        cx.run_until_parked();
3175        cx.simulate_keystrokes("enter");
3176        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
3177
3178        cx.simulate_keystrokes("x");
3179        cx.simulate_keystrokes("s");
3180        cx.run_until_parked();
3181        cx.simulate_keystrokes("enter");
3182        cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
3183
3184        // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
3185        // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
3186        // cx.simulate_keystrokes("s o n e enter");
3187        // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
3188    }
3189
3190    #[gpui::test]
3191    async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
3192        let mut cx = VimTestContext::new(cx, true).await;
3193
3194        cx.set_state("ˇhello two one two one two one", Mode::Visual);
3195        cx.simulate_keystrokes("/ o n e");
3196        cx.simulate_keystrokes("enter");
3197        cx.simulate_keystrokes("n n");
3198        cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
3199
3200        cx.set_state("ˇhello two one two one two one", Mode::Normal);
3201        cx.simulate_keystrokes("/ o n e");
3202        cx.simulate_keystrokes("enter");
3203        cx.simulate_keystrokes("n n");
3204        cx.assert_state("hello two one two one two ˇone", Mode::Normal);
3205
3206        cx.set_state("ˇhello two one two one two one", Mode::Normal);
3207        cx.simulate_keystrokes("/ o n e");
3208        cx.simulate_keystrokes("enter");
3209        cx.simulate_keystrokes("n g n g n");
3210        cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
3211
3212        cx.enable_helix();
3213
3214        cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
3215        cx.simulate_keystrokes("/ o n e");
3216        cx.simulate_keystrokes("enter");
3217        cx.simulate_keystrokes("n n");
3218        cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
3219
3220        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
3221        cx.simulate_keystrokes("/ o n e");
3222        cx.simulate_keystrokes("enter");
3223        cx.simulate_keystrokes("n n");
3224        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
3225    }
3226
3227    #[gpui::test]
3228    async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
3229        let mut cx = VimTestContext::new(cx, true).await;
3230        cx.enable_helix();
3231
3232        // Three occurrences of "one". After selecting all three with `n n`,
3233        // pressing `n` again wraps the search to the first occurrence.
3234        // The prior selections (at higher offsets) are chained before the
3235        // wrapped selection (at a lower offset), producing unsorted anchors
3236        // that cause `rope::Cursor::summary` to panic with
3237        // "cannot summarize backward".
3238        cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
3239        cx.simulate_keystrokes("/ o n e");
3240        cx.simulate_keystrokes("enter");
3241        cx.simulate_keystrokes("n n n");
3242        // Should not panic; all three occurrences should remain selected.
3243        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
3244    }
3245
3246    #[gpui::test]
3247    async fn test_helix_select_next_match_wrapping_from_normal(cx: &mut gpui::TestAppContext) {
3248        let mut cx = VimTestContext::new(cx, true).await;
3249        cx.enable_helix();
3250
3251        // Exact repro for #51573: start in HelixNormal, search, then `v` to
3252        // enter HelixSelect, then `n` past last match.
3253        //
3254        // In HelixNormal, search collapses the cursor to the match start.
3255        // Pressing `v` expands by only one character, creating a partial
3256        // selection that overlaps the full match range when the search wraps.
3257        // The overlapping ranges must be merged (not just deduped) to avoid
3258        // a backward-seeking rope cursor panic.
3259        cx.set_state(
3260            indoc! {"
3261                searˇch term
3262                stuff
3263                search term
3264                other stuff
3265            "},
3266            Mode::HelixNormal,
3267        );
3268        cx.simulate_keystrokes("/ t e r m");
3269        cx.simulate_keystrokes("enter");
3270        cx.simulate_keystrokes("v");
3271        cx.simulate_keystrokes("n");
3272        cx.simulate_keystrokes("n");
3273        // Should not panic when wrapping past last match.
3274        cx.assert_state(
3275            indoc! {"
3276                search «termˇ»
3277                stuff
3278                search «termˇ»
3279                other stuff
3280            "},
3281            Mode::HelixSelect,
3282        );
3283    }
3284
3285    #[gpui::test]
3286    async fn test_helix_select_star_then_match(cx: &mut gpui::TestAppContext) {
3287        let mut cx = VimTestContext::new(cx, true).await;
3288        cx.enable_helix();
3289
3290        // Repro attempts for #52852: `*` searches for word under cursor,
3291        // `v` enters select, `n` accumulates matches, `m` triggers match mode.
3292        // Try multiple cursor positions and match counts.
3293
3294        // Cursor on first occurrence, 3 more occurrences to select through
3295        cx.set_state(
3296            indoc! {"
3297                ˇone two one three one four one
3298            "},
3299            Mode::HelixNormal,
3300        );
3301        cx.simulate_keystrokes("*");
3302        cx.simulate_keystrokes("v");
3303        cx.simulate_keystrokes("n n n");
3304        // Should not panic on wrapping `n`.
3305
3306        // Cursor in the middle of text before matches
3307        cx.set_state(
3308            indoc! {"
3309                heˇllo one two one three one
3310            "},
3311            Mode::HelixNormal,
3312        );
3313        cx.simulate_keystrokes("*");
3314        cx.simulate_keystrokes("v");
3315        cx.simulate_keystrokes("n");
3316        // Should not panic.
3317
3318        // The original #52852 sequence: * v n n n then m m
3319        cx.set_state(
3320            indoc! {"
3321                fn ˇfoo() { bar(foo()) }
3322                fn baz() { foo() }
3323            "},
3324            Mode::HelixNormal,
3325        );
3326        cx.simulate_keystrokes("*");
3327        cx.simulate_keystrokes("v");
3328        cx.simulate_keystrokes("n n n");
3329        cx.simulate_keystrokes("m m");
3330        // Should not panic.
3331    }
3332
3333    #[gpui::test]
3334    async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
3335        let mut cx = VimTestContext::new(cx, true).await;
3336
3337        cx.set_state("ˇone two", Mode::HelixNormal);
3338        cx.simulate_keystrokes("c");
3339        cx.assert_state("ˇne two", Mode::Insert);
3340
3341        cx.set_state("«oneˇ» two", Mode::HelixNormal);
3342        cx.simulate_keystrokes("c");
3343        cx.assert_state("ˇ two", Mode::Insert);
3344
3345        cx.set_state(
3346            indoc! {"
3347            oneˇ two
3348            three
3349            "},
3350            Mode::HelixNormal,
3351        );
3352        cx.simulate_keystrokes("x c");
3353        cx.assert_state(
3354            indoc! {"
3355            ˇ
3356            three
3357            "},
3358            Mode::Insert,
3359        );
3360
3361        cx.set_state(
3362            indoc! {"
3363            one twoˇ
3364            three
3365            "},
3366            Mode::HelixNormal,
3367        );
3368        cx.simulate_keystrokes("c");
3369        cx.assert_state(
3370            indoc! {"
3371            one twoˇthree
3372            "},
3373            Mode::Insert,
3374        );
3375
3376        // Helix doesn't set the cursor to the first non-blank one when
3377        // replacing lines: it uses language-dependent indent queries instead.
3378        cx.set_state(
3379            indoc! {"
3380            one two
3381            «    indented
3382            three not indentedˇ»
3383            "},
3384            Mode::HelixNormal,
3385        );
3386        cx.simulate_keystrokes("c");
3387        cx.set_state(
3388            indoc! {"
3389            one two
3390            ˇ
3391            "},
3392            Mode::Insert,
3393        );
3394    }
3395
3396    #[gpui::test]
3397    async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
3398        let mut cx = VimTestContext::new(cx, true).await;
3399        cx.enable_helix();
3400
3401        // Test g l moves to last character, not after it
3402        cx.set_state("hello ˇworld!", Mode::HelixNormal);
3403        cx.simulate_keystrokes("g l");
3404        cx.assert_state("hello worldˇ!", Mode::HelixNormal);
3405
3406        // Test with Chinese characters, test if work with UTF-8?
3407        cx.set_state("ˇ你好世界", Mode::HelixNormal);
3408        cx.simulate_keystrokes("g l");
3409        cx.assert_state("你好世ˇ界", Mode::HelixNormal);
3410
3411        // Test with end of line
3412        cx.set_state("endˇ", Mode::HelixNormal);
3413        cx.simulate_keystrokes("g l");
3414        cx.assert_state("enˇd", Mode::HelixNormal);
3415
3416        // Test with empty line
3417        cx.set_state(
3418            indoc! {"
3419                hello
3420                ˇ
3421                world"},
3422            Mode::HelixNormal,
3423        );
3424        cx.simulate_keystrokes("g l");
3425        cx.assert_state(
3426            indoc! {"
3427                hello
3428                ˇ
3429                world"},
3430            Mode::HelixNormal,
3431        );
3432
3433        // Test with multiple lines
3434        cx.set_state(
3435            indoc! {"
3436                ˇfirst line
3437                second line
3438                third line"},
3439            Mode::HelixNormal,
3440        );
3441        cx.simulate_keystrokes("g l");
3442        cx.assert_state(
3443            indoc! {"
3444                first linˇe
3445                second line
3446                third line"},
3447            Mode::HelixNormal,
3448        );
3449    }
3450
3451    #[gpui::test]
3452    async fn test_helix_jump_starts_operator(cx: &mut gpui::TestAppContext) {
3453        let mut cx = VimTestContext::new(cx, true).await;
3454        cx.enable_helix();
3455        cx.set_state("ˇhello world\njump labels", Mode::HelixNormal);
3456
3457        cx.simulate_keystrokes("g w");
3458
3459        assert!(
3460            matches!(cx.active_operator(), Some(Operator::HelixJump { .. })),
3461            "expected HelixJump operator to be active"
3462        )
3463    }
3464
3465    #[gpui::test]
3466    async fn test_helix_jump_cancels_on_escape(cx: &mut gpui::TestAppContext) {
3467        let mut cx = VimTestContext::new(cx, true).await;
3468        cx.enable_helix();
3469        cx.set_state("ˇhello world\njump labels", Mode::HelixNormal);
3470        let overlay_counts = active_helix_jump_overlay_counts(&mut cx);
3471
3472        cx.simulate_keystrokes("g w");
3473        cx.simulate_keystrokes("escape");
3474
3475        cx.assert_state("ˇhello world\njump labels", Mode::HelixNormal);
3476        assert_helix_jump_cleared(&mut cx, overlay_counts);
3477    }
3478
3479    #[gpui::test]
3480    async fn test_helix_jump_cancels_on_invalid_first_char(cx: &mut gpui::TestAppContext) {
3481        let mut cx = VimTestContext::new(cx, true).await;
3482        cx.enable_helix();
3483        cx.set_state("ˇalpha beta gamma", Mode::HelixNormal);
3484        let overlay_counts = active_helix_jump_overlay_counts(&mut cx);
3485
3486        cx.simulate_keystrokes("g w");
3487        cx.simulate_keystrokes("z");
3488
3489        cx.assert_state("ˇalpha beta gamma", Mode::HelixNormal);
3490        assert_helix_jump_cleared(&mut cx, overlay_counts);
3491    }
3492
3493    #[gpui::test]
3494    async fn test_helix_jump_cancels_on_invalid_second_char(cx: &mut gpui::TestAppContext) {
3495        let mut cx = VimTestContext::new(cx, true).await;
3496        cx.enable_helix();
3497        cx.set_state("ˇalpha beta gamma", Mode::HelixNormal);
3498        let overlay_counts = active_helix_jump_overlay_counts(&mut cx);
3499
3500        cx.simulate_keystrokes("g w");
3501        cx.simulate_keystrokes("a z");
3502
3503        cx.assert_state("ˇalpha beta gamma", Mode::HelixNormal);
3504        assert_helix_jump_cleared(&mut cx, overlay_counts);
3505    }
3506
3507    #[gpui::test]
3508    async fn test_helix_jump_keeps_full_overlay_after_first_key(cx: &mut gpui::TestAppContext) {
3509        let mut cx = VimTestContext::new(cx, true).await;
3510        cx.enable_helix();
3511        let text = format!(
3512            "ˇ{}",
3513            (0..28)
3514                .map(|index| format!("w{index:02}"))
3515                .collect::<Vec<_>>()
3516                .join(" ")
3517        );
3518        cx.set_state(&text, Mode::HelixNormal);
3519
3520        cx.simulate_keystrokes("g w");
3521        let labels = active_helix_jump_labels(&mut cx);
3522        let initial_overlay_counts = active_helix_jump_overlay_counts(&mut cx);
3523        let first_group = labels
3524            .first()
3525            .and_then(|(label, _)| label.chars().next())
3526            .expect("expected at least one helix jump label");
3527        let next_group = labels
3528            .iter()
3529            .filter_map(|(label, _)| label.chars().next())
3530            .find(|ch| *ch != first_group)
3531            .expect("expected labels spanning more than one first-character group");
3532
3533        cx.simulate_keystrokes(&next_group.to_string());
3534
3535        assert_eq!(
3536            active_helix_jump_overlay_counts(&mut cx),
3537            initial_overlay_counts
3538        );
3539        assert!(
3540            matches!(
3541                cx.active_operator(),
3542                Some(Operator::HelixJump {
3543                    first_char: Some(ch),
3544                    ..
3545                }) if ch == next_group
3546            ),
3547            "expected HelixJump operator to keep the first typed label character"
3548        );
3549    }
3550
3551    #[gpui::test]
3552    async fn test_helix_jump_includes_word_before_cursor_boundary(cx: &mut gpui::TestAppContext) {
3553        let mut cx = VimTestContext::new(cx, true).await;
3554        cx.enable_helix();
3555        cx.set_state("oneˇ two three", Mode::HelixNormal);
3556
3557        jump_to_word(&mut cx, "one");
3558
3559        cx.assert_state("«oneˇ» two three", Mode::HelixNormal);
3560        assert_eq!(cx.active_operator(), None);
3561    }
3562
3563    #[gpui::test]
3564    async fn test_helix_jump_skips_single_char_words(cx: &mut gpui::TestAppContext) {
3565        let mut cx = VimTestContext::new(cx, true).await;
3566        cx.enable_helix();
3567        cx.set_state("ˇa bb c dd e", Mode::HelixNormal);
3568
3569        let words = helix_jump_labels_for_full_buffer(&mut cx)
3570            .into_iter()
3571            .map(|(_, word)| word)
3572            .collect::<Vec<_>>();
3573
3574        assert_eq!(words, vec!["bb".to_string(), "dd".to_string()]);
3575    }
3576
3577    #[gpui::test]
3578    async fn test_helix_jump_handles_underscored_words(cx: &mut gpui::TestAppContext) {
3579        let mut cx = VimTestContext::new(cx, true).await;
3580        cx.enable_helix();
3581        cx.set_state("baz quxˇ foo_bar _private", Mode::HelixNormal);
3582
3583        let words = helix_jump_labels_for_full_buffer(&mut cx)
3584            .into_iter()
3585            .map(|(_, word)| word)
3586            .collect::<Vec<_>>();
3587
3588        assert!(words.iter().any(|word| word == "foo_bar"));
3589        assert!(words.iter().any(|word| word == "_private"));
3590        assert!(!words.iter().any(|word| word == "foo"));
3591        assert!(!words.iter().any(|word| word == "bar"));
3592    }
3593
3594    #[gpui::test]
3595    async fn test_helix_jump_at_end_of_buffer(cx: &mut gpui::TestAppContext) {
3596        let mut cx = VimTestContext::new(cx, true).await;
3597        cx.enable_helix();
3598        cx.set_state("alpha beta gammaˇ", Mode::HelixNormal);
3599
3600        jump_to_word(&mut cx, "gamma");
3601
3602        cx.assert_state("alpha beta «gammaˇ»", Mode::HelixNormal);
3603        assert_eq!(cx.active_operator(), None);
3604    }
3605
3606    #[gpui::test]
3607    async fn test_helix_jump_moves_to_target_word(cx: &mut gpui::TestAppContext) {
3608        let mut cx = VimTestContext::new(cx, true).await;
3609        cx.enable_helix();
3610        cx.set_state("ˇone two three", Mode::HelixNormal);
3611
3612        jump_to_word(&mut cx, "three");
3613
3614        cx.assert_state("one two «threeˇ»", Mode::HelixNormal);
3615        assert_eq!(cx.active_operator(), None);
3616    }
3617
3618    #[gpui::test]
3619    async fn test_helix_jump_includes_line_selection_targets(cx: &mut gpui::TestAppContext) {
3620        let mut cx = VimTestContext::new(cx, true).await;
3621        cx.enable_helix();
3622        cx.set_state("alpha beta\nˇfoo bar baz\nqux quux", Mode::HelixNormal);
3623
3624        cx.simulate_keystrokes("x");
3625        jump_to_word(&mut cx, "bar");
3626
3627        cx.assert_state("alpha beta\nfoo «barˇ» baz\nqux quux", Mode::HelixNormal);
3628        assert_eq!(cx.active_operator(), None);
3629    }
3630
3631    #[gpui::test]
3632    async fn test_vim_jump_moves_to_target_word_start(cx: &mut gpui::TestAppContext) {
3633        let mut cx = VimTestContext::new(cx, true).await;
3634        bind_vim_jump_to_word(&mut cx, "g z");
3635        cx.set_state("ˇone two three", Mode::Normal);
3636
3637        jump_to_word_with_keystrokes(&mut cx, "g z", "two");
3638
3639        cx.assert_state("one ˇtwo three", Mode::Normal);
3640        assert_eq!(cx.active_operator(), None);
3641    }
3642
3643    #[gpui::test]
3644    async fn test_vim_jump_keeps_normal_cursor_shape(cx: &mut gpui::TestAppContext) {
3645        let mut cx = VimTestContext::new(cx, true).await;
3646        bind_vim_jump_to_word(&mut cx, "g z");
3647        cx.update(|_, cx| {
3648            SettingsStore::update_global(cx, |store, cx| {
3649                store.update_user_settings(cx, |settings| {
3650                    settings.vim.get_or_insert_default().cursor_shape =
3651                        Some(settings::CursorShapeSettings {
3652                            normal: Some(settings::CursorShape::Bar),
3653                            ..Default::default()
3654                        });
3655                });
3656            });
3657        });
3658        cx.set_state("ˇone two three", Mode::Normal);
3659
3660        cx.simulate_keystrokes("g z");
3661
3662        assert!(
3663            matches!(cx.active_operator(), Some(Operator::HelixJump { .. })),
3664            "expected HelixJump operator to be active"
3665        );
3666        cx.update_editor(|editor, _, _| {
3667            assert_eq!(editor.cursor_shape(), CursorShape::Bar);
3668        });
3669    }
3670
3671    #[gpui::test]
3672    async fn test_vim_visual_jump_extends_selection(cx: &mut gpui::TestAppContext) {
3673        let mut cx = VimTestContext::new(cx, true).await;
3674        bind_vim_jump_to_word(&mut cx, "g z");
3675        cx.set_state("one «twoˇ» three four", Mode::Visual);
3676
3677        jump_to_word_with_keystrokes(&mut cx, "g z", "three");
3678
3679        cx.assert_state("one «two tˇ»hree four", Mode::Visual);
3680        assert_eq!(cx.active_operator(), None);
3681    }
3682
3683    #[gpui::test]
3684    async fn test_vim_visual_jump_extends_selection_backward(cx: &mut gpui::TestAppContext) {
3685        let mut cx = VimTestContext::new(cx, true).await;
3686        bind_vim_jump_to_word(&mut cx, "g z");
3687        cx.set_state("one two «threeˇ» four", Mode::Visual);
3688
3689        jump_to_word_with_keystrokes(&mut cx, "g z", "one");
3690
3691        cx.assert_state("«ˇone two three» four", Mode::Visual);
3692        assert_eq!(cx.active_operator(), None);
3693    }
3694
3695    #[gpui::test]
3696    async fn test_helix_jump_extends_selection_forward(cx: &mut gpui::TestAppContext) {
3697        let mut cx = VimTestContext::new(cx, true).await;
3698        cx.enable_helix();
3699        cx.set_state("one «twoˇ» three four", Mode::HelixSelect);
3700
3701        jump_to_word(&mut cx, "four");
3702
3703        cx.assert_state("one «two three fourˇ»", Mode::HelixSelect);
3704        assert_eq!(cx.active_operator(), None);
3705    }
3706
3707    #[gpui::test]
3708    async fn test_helix_jump_extends_selection_backward_from_forward_selection(
3709        cx: &mut gpui::TestAppContext,
3710    ) {
3711        let mut cx = VimTestContext::new(cx, true).await;
3712        cx.enable_helix();
3713        cx.set_state("one «twoˇ» three four", Mode::HelixSelect);
3714
3715        jump_to_word(&mut cx, "one");
3716
3717        cx.assert_state("«ˇone two» three four", Mode::HelixSelect);
3718        assert_eq!(cx.active_operator(), None);
3719    }
3720
3721    #[gpui::test]
3722    async fn test_helix_jump_extends_reversed_selection_backward(cx: &mut gpui::TestAppContext) {
3723        let mut cx = VimTestContext::new(cx, true).await;
3724        cx.enable_helix();
3725        cx.set_state("one two «ˇthree» four", Mode::HelixSelect);
3726
3727        jump_to_word(&mut cx, "one");
3728
3729        cx.assert_state("«ˇone two three» four", Mode::HelixSelect);
3730        assert_eq!(cx.active_operator(), None);
3731    }
3732
3733    #[gpui::test]
3734    async fn test_helix_jump_prioritizes_nearby_targets_before_truncating(
3735        cx: &mut gpui::TestAppContext,
3736    ) {
3737        let mut cx = VimTestContext::new(cx, true).await;
3738        cx.enable_helix();
3739
3740        let cursor_index = 850usize;
3741        let target_word = format!("w{:03}", cursor_index + 1);
3742        let early_word = "w010".to_string();
3743        let text = (0..900usize)
3744            .map(|index| {
3745                let word = format!("w{index:03}");
3746                if index == cursor_index {
3747                    format!("ˇ{word}")
3748                } else {
3749                    word
3750                }
3751            })
3752            .collect::<Vec<_>>()
3753            .join(" ");
3754        cx.set_state(&text, Mode::HelixNormal);
3755
3756        let labels = helix_jump_labels_for_full_buffer(&mut cx);
3757
3758        assert_eq!(labels.len(), HELIX_JUMP_LABEL_LIMIT);
3759        assert!(
3760            labels.iter().any(|(_, word)| word == &target_word),
3761            "expected nearby target {target_word:?} to survive truncation"
3762        );
3763        assert!(
3764            !labels.iter().any(|(_, word)| word == &early_word),
3765            "expected distant early target {early_word:?} to be truncated first"
3766        );
3767    }
3768
3769    #[gpui::test]
3770    async fn test_helix_jump_label_ordering_alternates_directions(cx: &mut gpui::TestAppContext) {
3771        let mut cx = VimTestContext::new(cx, true).await;
3772        cx.enable_helix();
3773        cx.set_state("aaa bbb ccc ˇddd eee fff ggg", Mode::HelixNormal);
3774
3775        let first_labels = helix_jump_labels_for_full_buffer(&mut cx)
3776            .into_iter()
3777            .take(6)
3778            .collect::<Vec<_>>();
3779
3780        assert_eq!(
3781            first_labels,
3782            vec![
3783                ("aa".to_string(), "eee".to_string()),
3784                ("ab".to_string(), "ccc".to_string()),
3785                ("ac".to_string(), "fff".to_string()),
3786                ("ad".to_string(), "bbb".to_string()),
3787                ("ae".to_string(), "ggg".to_string()),
3788                ("af".to_string(), "aaa".to_string()),
3789            ]
3790        );
3791    }
3792
3793    #[gpui::test]
3794    async fn test_helix_jump_uses_theme_label_color(cx: &mut gpui::TestAppContext) {
3795        let mut cx = VimTestContext::new(cx, true).await;
3796        cx.enable_helix();
3797        cx.update(|_, cx| {
3798            SettingsStore::update_global(cx, |store, cx| {
3799                store.update_user_settings(cx, |settings| {
3800                    settings.theme.experimental_theme_overrides = Some(ThemeStyleContent {
3801                        colors: ThemeColorsContent {
3802                            vim_helix_jump_label_foreground: Some("#00ff00".to_string()),
3803                            ..Default::default()
3804                        },
3805                        ..Default::default()
3806                    });
3807                });
3808            });
3809        });
3810        cx.executor().advance_clock(Duration::from_millis(200));
3811        cx.run_until_parked();
3812
3813        let configured_label_color =
3814            cx.update(|_, cx| cx.theme().colors().vim_helix_jump_label_foreground);
3815        assert_ne!(
3816            configured_label_color,
3817            cx.update(|_, cx| cx.theme().status().error)
3818        );
3819        cx.set_state("ˇalpha beta gamma", Mode::HelixNormal);
3820
3821        let label_colors = cx.update_editor(|editor, window, cx| {
3822            let snapshot = editor.snapshot(window, cx);
3823            let display_snapshot = &snapshot.display_snapshot;
3824            let buffer_snapshot = display_snapshot.buffer_snapshot();
3825            let selections = editor.selections.all::<Point>(display_snapshot);
3826            let skip_data = Vim::selection_skip_offsets(buffer_snapshot, &selections, false);
3827            let cursor_offset = selections
3828                .first()
3829                .map(|selection| buffer_snapshot.point_to_offset(selection.head()))
3830                .unwrap_or(MultiBufferOffset(0));
3831            let style = editor.style(cx);
3832            let font = style.text.font();
3833            let font_size = style.text.font_size.to_pixels(window.rem_size());
3834            let data = Vim::build_helix_jump_ui_data(
3835                buffer_snapshot,
3836                MultiBufferOffset(0),
3837                buffer_snapshot.len(),
3838                cursor_offset,
3839                configured_label_color,
3840                &skip_data,
3841                window.text_system(),
3842                font,
3843                font_size,
3844            );
3845
3846            data.overlays
3847                .into_iter()
3848                .map(|overlay| overlay.label.text_color)
3849                .collect::<Vec<_>>()
3850        });
3851
3852        assert!(!label_colors.is_empty());
3853        assert!(
3854            label_colors
3855                .into_iter()
3856                .all(|color| color == configured_label_color)
3857        );
3858    }
3859
3860    #[gpui::test]
3861    async fn test_helix_jump_input_is_case_insensitive(cx: &mut gpui::TestAppContext) {
3862        let mut cx = VimTestContext::new(cx, true).await;
3863        cx.enable_helix();
3864        cx.set_state("ˇone two three", Mode::HelixNormal);
3865
3866        cx.simulate_keystrokes("g w");
3867        let label = helix_jump_label_for_word(&mut cx, "three");
3868        let mut chars = label.chars();
3869        let first = chars
3870            .next()
3871            .expect("jump labels are two characters long")
3872            .to_ascii_uppercase();
3873        let second = chars
3874            .next()
3875            .expect("jump labels are two characters long")
3876            .to_ascii_uppercase();
3877
3878        cx.simulate_keystrokes(&format!("{first} {second}"));
3879
3880        cx.assert_state("one two «threeˇ»", Mode::HelixNormal);
3881        assert_eq!(cx.active_operator(), None);
3882    }
3883
3884    #[gpui::test]
3885    async fn test_helix_jump_with_unicode_words(cx: &mut gpui::TestAppContext) {
3886        let mut cx = VimTestContext::new(cx, true).await;
3887        cx.enable_helix();
3888        cx.set_state("ˇcafé résumé naïve", Mode::HelixNormal);
3889
3890        jump_to_word(&mut cx, "naïve");
3891
3892        cx.assert_state("café résumé «naïveˇ»", Mode::HelixNormal);
3893        assert_eq!(cx.active_operator(), None);
3894    }
3895
3896    #[gpui::test]
3897    async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
3898        VimTestContext::init(cx);
3899
3900        let fs = FakeFs::new(cx.background_executor.clone());
3901        fs.insert_tree(
3902            path!("/dir"),
3903            json!({
3904                "file_a.rs": "// File A.",
3905                "file_b.rs": "// File B.",
3906            }),
3907        )
3908        .await;
3909
3910        let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3911        let window_handle =
3912            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3913        let workspace = window_handle
3914            .read_with(cx, |mw, _| mw.workspace().clone())
3915            .unwrap();
3916
3917        cx.update(|cx| {
3918            VimTestContext::init_keybindings(true, cx);
3919            SettingsStore::update_global(cx, |store, cx| {
3920                store.update_user_settings(cx, |store| store.helix_mode = Some(true));
3921            })
3922        });
3923
3924        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
3925
3926        workspace.update_in(cx, |workspace, window, cx| {
3927            ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
3928        });
3929
3930        let search_view = workspace.update_in(cx, |workspace, _, cx| {
3931            workspace
3932                .active_pane()
3933                .read(cx)
3934                .items()
3935                .find_map(|item| item.downcast::<ProjectSearchView>())
3936                .expect("Project search view should be active")
3937        });
3938
3939        project_search::perform_project_search(&search_view, "File A", cx);
3940
3941        search_view.update(cx, |search_view, cx| {
3942            let vim_mode = search_view
3943                .results_editor()
3944                .read(cx)
3945                .addon::<VimAddon>()
3946                .map(|addon| addon.entity.read(cx).mode);
3947
3948            assert_eq!(vim_mode, Some(Mode::HelixNormal));
3949        });
3950    }
3951
3952    #[gpui::test]
3953    async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
3954        let mut cx = VimTestContext::new(cx, true).await;
3955        cx.enable_helix();
3956
3957        // Start with a selection
3958        cx.set_state(
3959            indoc! {"
3960            «lineˇ» one
3961            line two
3962            line three
3963            line four
3964            line five"},
3965            Mode::HelixNormal,
3966        );
3967
3968        // Scroll down, selection should collapse
3969        cx.simulate_keystrokes("ctrl-d");
3970        cx.assert_state(
3971            indoc! {"
3972            line one
3973            line two
3974            line three
3975            line four
3976            line fiveˇ"},
3977            Mode::HelixNormal,
3978        );
3979
3980        // Make a new selection
3981        cx.simulate_keystroke("b");
3982        cx.assert_state(
3983            indoc! {"
3984            line one
3985            line two
3986            line three
3987            line four
3988            line «ˇfive»"},
3989            Mode::HelixNormal,
3990        );
3991
3992        // And scroll up, once again collapsing the selection.
3993        cx.simulate_keystroke("ctrl-u");
3994        cx.assert_state(
3995            indoc! {"
3996            line one
3997            line two
3998            line three
3999            line ˇfour
4000            line five"},
4001            Mode::HelixNormal,
4002        );
4003
4004        // Enter select mode
4005        cx.simulate_keystroke("v");
4006        cx.assert_state(
4007            indoc! {"
4008            line one
4009            line two
4010            line three
4011            line «fˇ»our
4012            line five"},
4013            Mode::HelixSelect,
4014        );
4015
4016        // And now the selection should be kept/expanded.
4017        cx.simulate_keystroke("ctrl-d");
4018        cx.assert_state(
4019            indoc! {"
4020            line one
4021            line two
4022            line three
4023            line «four
4024            line fiveˇ»"},
4025            Mode::HelixSelect,
4026        );
4027    }
4028
4029    #[gpui::test]
4030    async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
4031        let mut cx = VimTestContext::new(cx, true).await;
4032        cx.enable_helix();
4033
4034        // Ensure that, when lines are selected using `x`, pressing `shift-a`
4035        // actually puts the cursor at the end of the selected lines and not at
4036        // the end of the line below.
4037        cx.set_state(
4038            indoc! {"
4039            line oˇne
4040            line two"},
4041            Mode::HelixNormal,
4042        );
4043
4044        cx.simulate_keystrokes("x");
4045        cx.assert_state(
4046            indoc! {"
4047            «line one
4048            ˇ»line two"},
4049            Mode::HelixNormal,
4050        );
4051
4052        cx.simulate_keystrokes("shift-a");
4053        cx.assert_state(
4054            indoc! {"
4055            line oneˇ
4056            line two"},
4057            Mode::Insert,
4058        );
4059
4060        cx.set_state(
4061            indoc! {"
4062            line «one
4063            lineˇ» two"},
4064            Mode::HelixNormal,
4065        );
4066
4067        cx.simulate_keystrokes("shift-a");
4068        cx.assert_state(
4069            indoc! {"
4070            line one
4071            line twoˇ"},
4072            Mode::Insert,
4073        );
4074    }
4075
4076    #[gpui::test]
4077    async fn test_helix_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
4078        let mut cx = VimTestContext::new(cx, true).await;
4079        cx.enable_helix();
4080
4081        cx.set_state("«Hällöˇ» Wörld", Mode::HelixNormal);
4082        cx.simulate_keystrokes("r 1");
4083        cx.assert_state("«11111ˇ» Wörld", Mode::HelixNormal);
4084
4085        cx.set_state("«e\u{301}ˇ»", Mode::HelixNormal);
4086        cx.simulate_keystrokes("r 1");
4087        cx.assert_state("«1ˇ»", Mode::HelixNormal);
4088
4089        cx.set_state("«🙂ˇ»", Mode::HelixNormal);
4090        cx.simulate_keystrokes("r 1");
4091        cx.assert_state("«1ˇ»", Mode::HelixNormal);
4092    }
4093
4094    #[gpui::test]
4095    async fn test_helix_start_of_document(cx: &mut gpui::TestAppContext) {
4096        let mut cx = VimTestContext::new(cx, true).await;
4097        cx.enable_helix();
4098
4099        // gg lands at column 0 of the first line, regardless of current column
4100        cx.set_state(
4101            indoc! {"
4102            foo
4103              barˇbaz"},
4104            Mode::HelixNormal,
4105        );
4106        cx.simulate_keystrokes("g g");
4107        cx.assert_state(
4108            indoc! {"
4109            ˇfoo
4110              barbaz"},
4111            Mode::HelixNormal,
4112        );
4113
4114        // gg with an active selection collapses to column 0 of the first line
4115        cx.set_state(
4116            indoc! {"
4117            foo
4118            «bar bazˇ»"},
4119            Mode::HelixNormal,
4120        );
4121        cx.simulate_keystrokes("g g");
4122        cx.assert_state(
4123            indoc! {"
4124            ˇfoo
4125            bar baz"},
4126            Mode::HelixNormal,
4127        );
4128
4129        // a count goes to that line number at column 0
4130        cx.set_state(
4131            indoc! {"
4132            line one
4133            line two
4134              line threeˇ"},
4135            Mode::HelixNormal,
4136        );
4137        cx.simulate_keystrokes("2 g g");
4138        cx.assert_state(
4139            indoc! {"
4140            line one
4141            ˇline two
4142              line three"},
4143            Mode::HelixNormal,
4144        );
4145
4146        // a count larger than the number of lines clips to the last line
4147        cx.set_state(
4148            indoc! {"
4149            line one
4150            line two
4151            ˇline three"},
4152            Mode::HelixNormal,
4153        );
4154        cx.simulate_keystrokes("9 9 9 g g");
4155        cx.assert_state(
4156            indoc! {"
4157            line one
4158            line two
4159            ˇline three"},
4160            Mode::HelixNormal,
4161        );
4162
4163        // v gg extends the selection backward to col 0 of the first line
4164        cx.set_state(
4165            indoc! {"
4166            line one
4167            ˇline two
4168            line three"},
4169            Mode::HelixNormal,
4170        );
4171        cx.simulate_keystrokes("v g g");
4172        cx.assert_state(
4173            indoc! {"
4174            «ˇline one
4175            l»ine two
4176            line three"},
4177            Mode::HelixSelect,
4178        );
4179
4180        // gg in select mode with a reversed selection extends further backward
4181        cx.set_state(
4182            indoc! {"
4183            line one
4184            line «ˇtwo»
4185            line three"},
4186            Mode::HelixSelect,
4187        );
4188        cx.simulate_keystrokes("g g");
4189        cx.assert_state(
4190            indoc! {"
4191            «ˇline one
4192            line two»
4193            line three"},
4194            Mode::HelixSelect,
4195        );
4196    }
4197
4198    #[gpui::test]
4199    async fn test_helix_end_of_document(cx: &mut gpui::TestAppContext) {
4200        let mut cx = VimTestContext::new(cx, true).await;
4201        cx.enable_helix();
4202
4203        // ge lands at column 0 of the last line, regardless of current column
4204        cx.set_state(
4205            indoc! {"
4206              fooˇbar
4207            baz"},
4208            Mode::HelixNormal,
4209        );
4210        cx.simulate_keystrokes("g e");
4211        cx.assert_state(
4212            indoc! {"
4213              foobar
4214            ˇbaz"},
4215            Mode::HelixNormal,
4216        );
4217
4218        // ge with an active selection collapses to column 0 of the last line
4219        cx.set_state(
4220            indoc! {"
4221            «foo barˇ»
4222            baz"},
4223            Mode::HelixNormal,
4224        );
4225        cx.simulate_keystrokes("g e");
4226        cx.assert_state(
4227            indoc! {"
4228            foo bar
4229            ˇbaz"},
4230            Mode::HelixNormal,
4231        );
4232
4233        // a count is ignored; ge always goes to the last line
4234        cx.set_state(
4235            indoc! {"
4236              line oneˇ
4237            line two
4238            line three"},
4239            Mode::HelixNormal,
4240        );
4241        cx.simulate_keystrokes("2 g e");
4242        cx.assert_state(
4243            indoc! {"
4244              line one
4245            line two
4246            ˇline three"},
4247            Mode::HelixNormal,
4248        );
4249
4250        // v ge extends the selection to col 0 of the last line
4251        cx.set_state(
4252            indoc! {"
4253            ˇline one
4254            line two
4255            line three"},
4256            Mode::HelixNormal,
4257        );
4258        cx.simulate_keystrokes("v g e");
4259        cx.assert_state(
4260            indoc! {"
4261            «line one
4262            line two
4263            lˇ»ine three"},
4264            Mode::HelixSelect,
4265        );
4266
4267        // ge in select mode with a reversed selection extends forward to the last line
4268        cx.set_state(
4269            indoc! {"
4270            line one
4271            line «ˇtwo»
4272            line three"},
4273            Mode::HelixSelect,
4274        );
4275        cx.simulate_keystrokes("g e");
4276        cx.assert_state(
4277            indoc! {"
4278            line one
4279            line tw«o
4280            lˇ»ine three"},
4281            Mode::HelixSelect,
4282        );
4283    }
4284
4285    #[gpui::test]
4286    async fn test_helix_go_to_hunk(cx: &mut gpui::TestAppContext) {
4287        let mut cx = VimTestContext::new(cx, true).await;
4288        cx.enable_helix();
4289
4290        cx.set_state(
4291            indoc! {"
4292            ˇone
4293            two
4294            three"},
4295            Mode::HelixNormal,
4296        );
4297        cx.set_head_text(indoc! {"
4298            one
4299            CHANGED
4300            three"});
4301        cx.run_until_parked();
4302
4303        cx.simulate_keystrokes("]");
4304        assert_eq!(
4305            cx.active_operator(),
4306            Some(Operator::HelixNext { around: true })
4307        );
4308
4309        cx.simulate_keystrokes("g");
4310        cx.assert_state(
4311            indoc! {"
4312            one
4313            ˇtwo
4314            three"},
4315            Mode::HelixNormal,
4316        );
4317        assert_eq!(cx.active_operator(), None);
4318
4319        cx.set_state(
4320            indoc! {"
4321            one
4322            two
4323            ˇthree"},
4324            Mode::HelixNormal,
4325        );
4326        cx.set_head_text(indoc! {"
4327            one
4328            CHANGED
4329            three"});
4330        cx.run_until_parked();
4331
4332        cx.simulate_keystrokes("[");
4333        assert_eq!(
4334            cx.active_operator(),
4335            Some(Operator::HelixPrevious { around: true })
4336        );
4337
4338        cx.simulate_keystrokes("g");
4339        cx.assert_state(
4340            indoc! {"
4341            one
4342            ˇtwo
4343            three"},
4344            Mode::HelixNormal,
4345        );
4346        assert_eq!(cx.active_operator(), None);
4347    }
4348
4349    #[gpui::test]
4350    async fn test_helix_rename_uses_visible_cursor_position(cx: &mut gpui::TestAppContext) {
4351        let mut cx = VimTestContext::new_typescript(cx).await;
4352        cx.enable_helix();
4353
4354        cx.set_state(
4355            "const before = 2; console.log(«beforeˇ»)",
4356            Mode::HelixNormal,
4357        );
4358
4359        let expected_position = cx.to_lsp(MultiBufferOffset(
4360            "const before = 2; console.log(befor".len(),
4361        ));
4362        let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)");
4363        let tgt_range = cx.lsp_range("const before = 2; console.log(«beforeˇ»)");
4364        let mut prepare_request = cx
4365            .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
4366                move |_, params, _| async move {
4367                    assert_eq!(params.position, expected_position);
4368                    Ok(Some(lsp::PrepareRenameResponse::Range(tgt_range)))
4369                },
4370            );
4371        let mut rename_request = cx.set_request_handler::<lsp::request::Rename, _, _>(
4372            move |url, params, _| async move {
4373                Ok(Some(lsp::WorkspaceEdit {
4374                    changes: Some(
4375                        [(
4376                            url.clone(),
4377                            vec![
4378                                lsp::TextEdit::new(def_range, params.new_name.clone()),
4379                                lsp::TextEdit::new(tgt_range, params.new_name),
4380                            ],
4381                        )]
4382                        .into(),
4383                    ),
4384                    ..Default::default()
4385                }))
4386            },
4387        );
4388
4389        cx.simulate_keystrokes("space r");
4390        prepare_request.next().await.unwrap();
4391        cx.simulate_input("after");
4392        cx.simulate_keystrokes("enter");
4393        rename_request.next().await.unwrap();
4394
4395        cx.assert_state("const after = 2; console.log(afterˇ)", Mode::HelixNormal);
4396    }
4397}
4398
Served at tenant.openagents/omega Member data and write actions are omitted.