Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:47:26.060Z 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

visual.rs

2055 lines · 74.7 KB · rust
1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5    Bias, DisplayPoint, Editor, MultiBufferOffset, SelectionEffects,
6    display_map::{DisplaySnapshot, ToDisplayPoint},
7    movement,
8};
9use gpui::{Context, Window, actions};
10use language::{Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use search::BufferSearchBar;
13use text::TransactionId;
14use util::ResultExt;
15use workspace::searchable::Direction;
16
17use crate::{
18    Vim,
19    motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
20    object::Object,
21    state::{Mark, Mode, Operator},
22};
23
24actions!(
25    vim,
26    [
27        /// Toggles visual mode.
28        ToggleVisual,
29        /// Toggles visual line mode.
30        ToggleVisualLine,
31        /// Toggles visual block mode.
32        ToggleVisualBlock,
33        /// Deletes the visual selection.
34        VisualDelete,
35        /// Deletes entire lines in visual selection.
36        VisualDeleteLine,
37        /// Yanks (copies) the visual selection.
38        VisualYank,
39        /// Yanks entire lines in visual selection.
40        VisualYankLine,
41        /// Moves cursor to the other end of the selection.
42        OtherEnd,
43        /// Moves cursor to the other end of the selection (row-aware).
44        OtherEndRowAware,
45        /// Selects the next occurrence of the current selection.
46        SelectNext,
47        /// Selects the previous occurrence of the current selection.
48        SelectPrevious,
49        /// Selects the next match of the current selection.
50        SelectNextMatch,
51        /// Selects the previous match of the current selection.
52        SelectPreviousMatch,
53        /// Selects the next smaller syntax node.
54        SelectSmallerSyntaxNode,
55        /// Selects the next larger syntax node.
56        SelectLargerSyntaxNode,
57        /// Selects the next syntax node sibling.
58        SelectNextSyntaxNode,
59        /// Selects the previous syntax node sibling.
60        SelectPreviousSyntaxNode,
61        /// Restores the previous visual selection.
62        RestoreVisualSelection,
63        /// Inserts at the end of each line in visual selection.
64        VisualInsertEndOfLine,
65        /// Inserts at the first non-whitespace character of each line.
66        VisualInsertFirstNonWhiteSpace,
67    ]
68);
69
70pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
71    Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| {
72        vim.toggle_mode(Mode::Visual, window, cx)
73    });
74    Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| {
75        vim.toggle_mode(Mode::VisualLine, window, cx)
76    });
77    Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| {
78        vim.toggle_mode(Mode::VisualBlock, window, cx)
79    });
80    Vim::action(editor, cx, Vim::other_end);
81    Vim::action(editor, cx, Vim::other_end_row_aware);
82    Vim::action(editor, cx, Vim::visual_insert_end_of_line);
83    Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
84    Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
85        vim.record_current_action(cx);
86        vim.visual_delete(false, window, cx);
87    });
88    Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| {
89        vim.record_current_action(cx);
90        vim.visual_delete(true, window, cx);
91    });
92    Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| {
93        vim.visual_yank(false, window, cx)
94    });
95    Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| {
96        vim.visual_yank(true, window, cx)
97    });
98
99    Vim::action(editor, cx, Vim::select_next);
100    Vim::action(editor, cx, Vim::select_previous);
101    Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| {
102        vim.select_match(Direction::Next, window, cx);
103    });
104    Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| {
105        vim.select_match(Direction::Prev, window, cx);
106    });
107
108    Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| {
109        let count = Vim::take_count(cx).unwrap_or(1);
110        Vim::take_forced_motion(cx);
111        for _ in 0..count {
112            vim.update_editor(cx, |_, editor, cx| {
113                editor.select_larger_syntax_node(&Default::default(), window, cx);
114            });
115        }
116    });
117
118    Vim::action(editor, cx, |vim, _: &SelectNextSyntaxNode, window, cx| {
119        let count = Vim::take_count(cx).unwrap_or(1);
120        Vim::take_forced_motion(cx);
121        for _ in 0..count {
122            vim.update_editor(cx, |_, editor, cx| {
123                editor.select_next_syntax_node(&Default::default(), window, cx);
124            });
125        }
126    });
127
128    Vim::action(
129        editor,
130        cx,
131        |vim, _: &SelectPreviousSyntaxNode, window, cx| {
132            let count = Vim::take_count(cx).unwrap_or(1);
133            Vim::take_forced_motion(cx);
134            for _ in 0..count {
135                vim.update_editor(cx, |_, editor, cx| {
136                    editor.select_prev_syntax_node(&Default::default(), window, cx);
137                });
138            }
139        },
140    );
141
142    Vim::action(
143        editor,
144        cx,
145        |vim, _: &SelectSmallerSyntaxNode, window, cx| {
146            let count = Vim::take_count(cx).unwrap_or(1);
147            Vim::take_forced_motion(cx);
148            for _ in 0..count {
149                vim.update_editor(cx, |_, editor, cx| {
150                    editor.select_smaller_syntax_node(&Default::default(), window, cx);
151                });
152            }
153        },
154    );
155
156    Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| {
157        let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
158            return;
159        };
160        let marks = vim
161            .update_editor(cx, |vim, editor, cx| {
162                vim.get_mark("<", editor, window, cx)
163                    .zip(vim.get_mark(">", editor, window, cx))
164            })
165            .flatten();
166        let Some((Mark::Local(start), Mark::Local(end))) = marks else {
167            return;
168        };
169        let ranges = start
170            .iter()
171            .zip(end)
172            .zip(reversed)
173            .map(|((start, end), reversed)| (*start, end, reversed))
174            .collect::<Vec<_>>();
175
176        if vim.mode.is_visual() {
177            vim.create_visual_marks(vim.mode, window, cx);
178        }
179
180        vim.update_editor(cx, |_, editor, cx| {
181            editor.set_clip_at_line_ends(false, cx);
182            editor.change_selections(Default::default(), window, cx, |s| {
183                let map = s.display_snapshot();
184                let ranges = ranges
185                    .into_iter()
186                    .map(|(start, end, reversed)| {
187                        let mut new_end =
188                            movement::saturating_right(&map, end.to_display_point(&map));
189                        let mut new_start = start.to_display_point(&map);
190                        if new_start >= new_end {
191                            if new_end.column() == 0 {
192                                new_end = movement::right(&map, new_end)
193                            } else {
194                                new_start = movement::saturating_left(&map, new_end);
195                            }
196                        }
197                        Selection {
198                            id: s.new_selection_id(),
199                            start: new_start.to_point(&map),
200                            end: new_end.to_point(&map),
201                            reversed,
202                            goal: SelectionGoal::None,
203                        }
204                    })
205                    .collect();
206                s.select(ranges);
207            })
208        });
209        vim.switch_mode(stored_mode, true, window, cx)
210    });
211}
212
213impl Vim {
214    pub fn visual_motion(
215        &mut self,
216        motion: Motion,
217        times: Option<usize>,
218        window: &mut Window,
219        cx: &mut Context<Self>,
220    ) {
221        self.update_editor(cx, |vim, editor, cx| {
222            let text_layout_details = editor.text_layout_details(window, cx);
223            if vim.mode == Mode::VisualBlock
224                && !matches!(
225                    motion,
226                    Motion::EndOfLine {
227                        display_lines: false
228                    }
229                )
230            {
231                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
232                vim.visual_block_motion(
233                    is_up_or_down,
234                    editor,
235                    window,
236                    cx,
237                    &mut |map, point, goal| {
238                        motion.move_point(map, point, goal, times, &text_layout_details)
239                    },
240                )
241            } else {
242                editor.change_selections(Default::default(), window, cx, |s| {
243                    s.move_with(&mut |map, selection| {
244                        let was_reversed = selection.reversed;
245                        let mut current_head = selection.head();
246
247                        // our motions assume the current character is after the cursor,
248                        // but in (forward) visual mode the current character is just
249                        // before the end of the selection.
250                        if !selection.reversed && !selection.is_empty() {
251                            current_head = movement::left(map, selection.end)
252                        }
253
254                        let Some((new_head, goal)) = motion.move_point(
255                            map,
256                            current_head,
257                            selection.goal,
258                            times,
259                            &text_layout_details,
260                        ) else {
261                            return;
262                        };
263
264                        selection.set_head(new_head, goal);
265
266                        // ensure the current character is included in the selection.
267                        if !selection.reversed {
268                            let next_point = if vim.mode == Mode::VisualBlock {
269                                movement::saturating_right(map, selection.end)
270                            } else {
271                                movement::right(map, selection.end)
272                            };
273
274                            selection.end = next_point;
275                        }
276
277                        // vim always ensures the anchor character stays selected.
278                        // if our selection has reversed, we need to move the opposite end
279                        // to ensure the anchor is still selected.
280                        if was_reversed && !selection.reversed {
281                            selection.start = movement::left(map, selection.start);
282                        } else if !was_reversed && selection.reversed {
283                            selection.end = movement::right(map, selection.end);
284                        }
285                    })
286                });
287            }
288        });
289    }
290
291    pub fn visual_block_motion(
292        &mut self,
293        preserve_goal: bool,
294        editor: &mut Editor,
295        window: &mut Window,
296        cx: &mut Context<Editor>,
297        move_selection: &mut dyn FnMut(
298            &DisplaySnapshot,
299            DisplayPoint,
300            SelectionGoal,
301        ) -> Option<(DisplayPoint, SelectionGoal)>,
302    ) {
303        let text_layout_details = editor.text_layout_details(window, cx);
304        editor.change_selections(Default::default(), window, cx, |s| {
305            let map = &s.display_snapshot();
306            let mut head = s.newest_anchor().head().to_display_point(map);
307            let mut tail = s.oldest_anchor().tail().to_display_point(map);
308
309            let mut head_x = map.x_for_display_point(head, &text_layout_details);
310            let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
311
312            let (start, end) = match s.newest_anchor().goal {
313                SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
314                SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
315                _ => (tail_x.into(), head_x.into()),
316            };
317            let mut goal = SelectionGoal::HorizontalRange { start, end };
318
319            let was_reversed = tail_x > head_x;
320            if !was_reversed && !preserve_goal {
321                head = movement::saturating_left(map, head);
322            }
323
324            let reverse_aware_goal = if was_reversed {
325                SelectionGoal::HorizontalRange {
326                    start: end,
327                    end: start,
328                }
329            } else {
330                goal
331            };
332
333            let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else {
334                return;
335            };
336            head = new_head;
337            head_x = map.x_for_display_point(head, &text_layout_details);
338
339            let is_reversed = tail_x > head_x;
340            if was_reversed && !is_reversed {
341                tail = movement::saturating_left(map, tail);
342                tail_x = map.x_for_display_point(tail, &text_layout_details);
343            } else if !was_reversed && is_reversed {
344                tail = movement::saturating_right(map, tail);
345                tail_x = map.x_for_display_point(tail, &text_layout_details);
346            }
347            if !is_reversed && !preserve_goal {
348                head = movement::saturating_right(map, head);
349                head_x = map.x_for_display_point(head, &text_layout_details);
350            }
351
352            let positions = if is_reversed {
353                head_x..tail_x
354            } else {
355                tail_x..head_x
356            };
357
358            if !preserve_goal {
359                goal = SelectionGoal::HorizontalRange {
360                    start: f64::from(positions.start),
361                    end: f64::from(positions.end),
362                };
363            }
364
365            let mut selections = Vec::new();
366            let mut row = tail.row();
367            let going_up = tail.row() > head.row();
368            let direction = if going_up { -1 } else { 1 };
369
370            loop {
371                let laid_out_line = map.layout_row(row, &text_layout_details);
372                let start = DisplayPoint::new(
373                    row,
374                    laid_out_line.closest_index_for_x(positions.start) as u32,
375                );
376                let mut end =
377                    DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
378                if end <= start {
379                    if start.column() == map.line_len(start.row()) {
380                        end = start;
381                    } else {
382                        end = movement::saturating_right(map, start);
383                    }
384                }
385
386                if positions.start <= laid_out_line.width {
387                    let selection = Selection {
388                        id: s.new_selection_id(),
389                        start: start.to_point(map),
390                        end: end.to_point(map),
391                        reversed: is_reversed &&
392                                    // For neovim parity: cursor is not reversed when column is a single character
393                                    end.column() - start.column() > 1,
394                        goal,
395                    };
396
397                    selections.push(selection);
398                }
399
400                // When dealing with soft wrapped lines, it's possible that
401                // `row` ends up being set to a value other than `head.row()` as
402                // `head.row()` might be a `DisplayPoint` mapped to a soft
403                // wrapped line, hence the need for `<=` and `>=` instead of
404                // `==`.
405                if going_up && row <= head.row() || !going_up && row >= head.row() {
406                    break;
407                }
408
409                // Find the next or previous buffer row where the `row` should
410                // be moved to, so that wrapped lines are skipped.
411                row = map
412                    .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
413                    .row();
414            }
415
416            s.select(selections);
417        })
418    }
419
420    pub fn visual_object(
421        &mut self,
422        object: Object,
423        count: Option<usize>,
424        window: &mut Window,
425        cx: &mut Context<Vim>,
426    ) {
427        if let Some(Operator::Object { around }) = self.active_operator() {
428            self.pop_operator(window, cx);
429            let current_mode = self.mode;
430            let target_mode = object.target_visual_mode(current_mode, around);
431            if target_mode != current_mode {
432                self.switch_mode(target_mode, true, window, cx);
433            }
434
435            self.update_editor(cx, |_, editor, cx| {
436                editor.change_selections(Default::default(), window, cx, |s| {
437                    s.move_with(&mut |map, selection| {
438                        let mut mut_selection = selection.clone();
439
440                        // all our motions assume that the current character is
441                        // after the cursor; however in the case of a visual selection
442                        // the current character is before the cursor.
443                        // But this will affect the judgment of the html tag
444                        // so the html tag needs to skip this logic.
445                        if !selection.reversed && object != Object::Tag {
446                            mut_selection.set_head(
447                                movement::left(map, mut_selection.head()),
448                                mut_selection.goal,
449                            );
450                        }
451
452                        let original_point = selection.tail().to_point(map);
453
454                        if let Some(range) = object.range(map, mut_selection, around, count) {
455                            if !range.is_empty() {
456                                let expand_both_ways = object.always_expands_both_ways()
457                                    || selection.is_empty()
458                                    || movement::right(map, selection.start) == selection.end;
459
460                                if expand_both_ways {
461                                    if selection.start == range.start
462                                        && selection.end == range.end
463                                        && object.always_expands_both_ways()
464                                    {
465                                        if let Some(range) =
466                                            object.range(map, selection.clone(), around, count)
467                                        {
468                                            selection.start = range.start;
469                                            selection.end = range.end;
470                                        }
471                                    } else {
472                                        selection.start = range.start;
473                                        selection.end = range.end;
474                                    }
475                                } else if selection.reversed {
476                                    selection.start = range.start;
477                                } else {
478                                    selection.end = range.end;
479                                }
480                            }
481
482                            // In the visual selection result of a paragraph object, the cursor is
483                            // placed at the start of the last line. And in the visual mode, the
484                            // selection end is located after the end character. So, adjustment of
485                            // selection end is needed.
486                            //
487                            // We don't do this adjustment for a one-line blank paragraph since the
488                            // trailing newline is included in its selection from the beginning.
489                            if object == Object::Paragraph && range.start != range.end {
490                                let row_of_selection_end_line = selection.end.to_point(map).row;
491                                let new_selection_end = if map
492                                    .buffer_snapshot()
493                                    .line_len(MultiBufferRow(row_of_selection_end_line))
494                                    == 0
495                                {
496                                    Point::new(row_of_selection_end_line + 1, 0)
497                                } else {
498                                    Point::new(row_of_selection_end_line, 1)
499                                };
500                                selection.end = new_selection_end.to_display_point(map);
501                            }
502
503                            // To match vim, if the range starts of the same line as it originally
504                            // did, we keep the tail of the selection in the same place instead of
505                            // snapping it to the start of the line
506                            if target_mode == Mode::VisualLine {
507                                let new_start_point = selection.start.to_point(map);
508                                if new_start_point.row == original_point.row {
509                                    if selection.end.to_point(map).row > new_start_point.row {
510                                        if original_point.column
511                                            == map
512                                                .buffer_snapshot()
513                                                .line_len(MultiBufferRow(original_point.row))
514                                        {
515                                            selection.start = movement::saturating_left(
516                                                map,
517                                                original_point.to_display_point(map),
518                                            )
519                                        } else {
520                                            selection.start = original_point.to_display_point(map)
521                                        }
522                                    } else {
523                                        let original_display_point =
524                                            original_point.to_display_point(map);
525                                        if selection.end <= original_display_point {
526                                            selection.end = movement::saturating_right(
527                                                map,
528                                                original_display_point,
529                                            );
530                                            if original_point.column > 0 {
531                                                selection.reversed = true
532                                            }
533                                        }
534                                    }
535                                }
536                            }
537                        }
538                    });
539                });
540            });
541        }
542    }
543
544    fn visual_insert_end_of_line(
545        &mut self,
546        _: &VisualInsertEndOfLine,
547        window: &mut Window,
548        cx: &mut Context<Self>,
549    ) {
550        self.update_editor(cx, |_, editor, cx| {
551            editor.split_selection_into_lines(&Default::default(), window, cx);
552            editor.change_selections(Default::default(), window, cx, |s| {
553                s.move_cursors_with(&mut |map, cursor, _| {
554                    (next_line_end(map, cursor, 1), SelectionGoal::None)
555                });
556            });
557        });
558
559        self.switch_mode(Mode::Insert, false, window, cx);
560    }
561
562    fn visual_insert_first_non_white_space(
563        &mut self,
564        _: &VisualInsertFirstNonWhiteSpace,
565        window: &mut Window,
566        cx: &mut Context<Self>,
567    ) {
568        self.update_editor(cx, |_, editor, cx| {
569            editor.split_selection_into_lines(&Default::default(), window, cx);
570            editor.change_selections(Default::default(), window, cx, |s| {
571                s.move_cursors_with(&mut |map, cursor, _| {
572                    (
573                        first_non_whitespace(map, false, cursor),
574                        SelectionGoal::None,
575                    )
576                });
577            });
578        });
579
580        self.switch_mode(Mode::Insert, false, window, cx);
581    }
582
583    fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
584        if self.mode == mode {
585            self.switch_mode(Mode::Normal, false, window, cx);
586        } else {
587            self.switch_mode(mode, false, window, cx);
588        }
589    }
590
591    pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
592        self.update_editor(cx, |_, editor, cx| {
593            editor.change_selections(Default::default(), window, cx, |s| {
594                s.move_with(&mut |_, selection| {
595                    selection.reversed = !selection.reversed;
596                });
597            })
598        });
599    }
600
601    pub fn other_end_row_aware(
602        &mut self,
603        _: &OtherEndRowAware,
604        window: &mut Window,
605        cx: &mut Context<Self>,
606    ) {
607        let mode = self.mode;
608        self.update_editor(cx, |_, editor, cx| {
609            editor.change_selections(Default::default(), window, cx, |s| {
610                s.move_with(&mut |_, selection| {
611                    selection.reversed = !selection.reversed;
612                });
613                if mode == Mode::VisualBlock {
614                    s.reverse_selections();
615                }
616            })
617        });
618    }
619
620    pub fn visual_delete(
621        &mut self,
622        line_mode: bool,
623        window: &mut Window,
624        cx: &mut Context<Self>,
625    ) -> Option<TransactionId> {
626        self.store_visual_marks(window, cx);
627        let transaction_id = self.update_editor(cx, |vim, editor, cx| {
628            let mut original_columns: HashMap<_, _> = Default::default();
629            let line_mode = line_mode || editor.selections.line_mode();
630            editor.selections.set_line_mode(false);
631
632            editor.transact(window, cx, |editor, window, cx| {
633                editor.change_selections(Default::default(), window, cx, |s| {
634                    s.move_with(&mut |map, selection| {
635                        if line_mode {
636                            let mut position = selection.head();
637                            if !selection.reversed {
638                                position = movement::left(map, position);
639                            }
640                            original_columns.insert(selection.id, position.to_point(map).column);
641                            if vim.mode == Mode::VisualBlock {
642                                *selection.end.column_mut() = map.line_len(selection.end.row())
643                            } else {
644                                let start = selection.start.to_point(map);
645                                let end = selection.end.to_point(map);
646                                selection.start = map.prev_line_boundary(start).1;
647                                if end.column == 0 && end > start {
648                                    let row = end.row.saturating_sub(1);
649                                    selection.end = Point::new(
650                                        row,
651                                        map.buffer_snapshot().line_len(MultiBufferRow(row)),
652                                    )
653                                    .to_display_point(map)
654                                } else {
655                                    selection.end = map.next_line_boundary(end).1;
656                                }
657                            }
658                        }
659                        selection.goal = SelectionGoal::None;
660                    });
661                });
662                let kind = if line_mode {
663                    MotionKind::Linewise
664                } else {
665                    MotionKind::Exclusive
666                };
667                vim.copy_selections_content(editor, kind, window, cx);
668
669                if line_mode && vim.mode != Mode::VisualBlock {
670                    editor.change_selections(Default::default(), window, cx, |s| {
671                        s.move_with(&mut |map, selection| {
672                            let end = selection.end.to_point(map);
673                            let start = selection.start.to_point(map);
674                            if end.row < map.buffer_snapshot().max_point().row {
675                                selection.end = Point::new(end.row + 1, 0).to_display_point(map)
676                            } else if start.row > 0 {
677                                selection.start = Point::new(
678                                    start.row - 1,
679                                    map.buffer_snapshot()
680                                        .line_len(MultiBufferRow(start.row - 1)),
681                                )
682                                .to_display_point(map)
683                            }
684                        });
685                    });
686                }
687                editor.delete_selections_with_linked_edits(window, cx);
688
689                // Fixup cursor position after the deletion. Helix keeps the
690                // cursor on the trailing newline, so only clip in Vim modes.
691                editor.set_clip_at_line_ends(!vim.mode.is_helix(), cx);
692                editor.change_selections(Default::default(), window, cx, |s| {
693                    s.move_with(&mut |map, selection| {
694                        let mut cursor = selection.head().to_point(map);
695
696                        if let Some(column) = original_columns.get(&selection.id) {
697                            cursor.column = *column
698                        }
699                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
700                        selection.collapse_to(cursor, selection.goal)
701                    });
702                    if vim.mode == Mode::VisualBlock {
703                        s.select_anchors(vec![s.first_anchor()])
704                    }
705                });
706            })
707        });
708        let transaction_id = transaction_id.flatten();
709        self.switch_mode(Mode::Normal, true, window, cx);
710        transaction_id
711    }
712
713    pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
714        self.store_visual_marks(window, cx);
715        self.update_editor(cx, |vim, editor, cx| {
716            let line_mode = line_mode || editor.selections.line_mode();
717
718            // For visual line mode, adjust selections to avoid yanking the next line when on \n
719            if line_mode && vim.mode != Mode::VisualBlock {
720                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
721                    s.move_with(&mut |map, selection| {
722                        let start = selection.start.to_point(map);
723                        let end = selection.end.to_point(map);
724                        if end.column == 0 && end > start {
725                            let row = end.row.saturating_sub(1);
726                            selection.end = Point::new(
727                                row,
728                                map.buffer_snapshot().line_len(MultiBufferRow(row)),
729                            )
730                            .to_display_point(map);
731                        }
732                    });
733                });
734            }
735
736            editor.selections.set_line_mode(line_mode);
737            let kind = if line_mode {
738                MotionKind::Linewise
739            } else {
740                MotionKind::Exclusive
741            };
742            vim.yank_selections_content(editor, kind, window, cx);
743            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
744                s.move_with(&mut |map, selection| {
745                    if line_mode {
746                        selection.start = start_of_line(map, false, selection.start);
747                    };
748                    selection.collapse_to(selection.start, SelectionGoal::None)
749                });
750                if vim.mode == Mode::VisualBlock {
751                    s.select_anchors(vec![s.first_anchor()])
752                }
753            });
754        });
755        self.switch_mode(Mode::Normal, true, window, cx);
756    }
757
758    pub(crate) fn visual_replace(
759        &mut self,
760        text: Arc<str>,
761        window: &mut Window,
762        cx: &mut Context<Self>,
763    ) {
764        self.stop_recording(cx);
765        self.update_editor(cx, |_, editor, cx| {
766            editor.transact(window, cx, |editor, window, cx| {
767                let display_map = editor.display_snapshot(cx);
768                let selections = editor.selections.all_adjusted_display(&display_map);
769
770                // Selections are biased right at the start. So we need to store
771                // anchors that are biased left so that we can restore the selections
772                // after the change
773                let stable_anchors = editor
774                    .selections
775                    .disjoint_anchors_arc()
776                    .iter()
777                    .map(|selection| {
778                        let start = selection.start.bias_left(&display_map.buffer_snapshot());
779                        start..start
780                    })
781                    .collect::<Vec<_>>();
782
783                let mut edits = Vec::new();
784                for selection in selections.iter() {
785                    let selection = selection.clone();
786                    for row_range in
787                        movement::split_display_range_by_lines(&display_map, selection.range())
788                    {
789                        let range = row_range.start.to_offset(&display_map, Bias::Right)
790                            ..row_range.end.to_offset(&display_map, Bias::Right);
791                        let grapheme_count = display_map
792                            .buffer_snapshot()
793                            .grapheme_count_for_range(&range);
794                        let text = text.repeat(grapheme_count);
795                        edits.push((range, text));
796                    }
797                }
798
799                editor.edit(edits, cx);
800                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
801                    s.select_ranges(stable_anchors)
802                });
803            });
804        });
805        self.switch_mode(Mode::Normal, false, window, cx);
806    }
807
808    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
809        Vim::take_forced_motion(cx);
810        let count =
811            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
812        self.update_editor(cx, |_, editor, cx| {
813            editor.set_clip_at_line_ends(false, cx);
814            for _ in 0..count {
815                if editor
816                    .select_next(&Default::default(), window, cx)
817                    .log_err()
818                    .is_none()
819                {
820                    break;
821                }
822            }
823        });
824    }
825
826    pub fn select_previous(
827        &mut self,
828        _: &SelectPrevious,
829        window: &mut Window,
830        cx: &mut Context<Self>,
831    ) {
832        Vim::take_forced_motion(cx);
833        let count =
834            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
835        self.update_editor(cx, |_, editor, cx| {
836            for _ in 0..count {
837                if editor
838                    .select_previous(&Default::default(), window, cx)
839                    .log_err()
840                    .is_none()
841                {
842                    break;
843                }
844            }
845        });
846    }
847
848    pub fn select_match(
849        &mut self,
850        direction: Direction,
851        window: &mut Window,
852        cx: &mut Context<Self>,
853    ) {
854        Vim::take_forced_motion(cx);
855        let count = Vim::take_count(cx).unwrap_or(1);
856        let Some(pane) = self.pane(window, cx) else {
857            return;
858        };
859        let vim_is_normal = self.mode == Mode::Normal;
860        let mut start_selection = MultiBufferOffset(0);
861        let mut end_selection = MultiBufferOffset(0);
862
863        self.update_editor(cx, |_, editor, _| {
864            editor.set_collapse_matches(false);
865        });
866        if vim_is_normal {
867            pane.update(cx, |pane, cx| {
868                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
869                {
870                    search_bar.update(cx, |search_bar, cx| {
871                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
872                            return;
873                        }
874                        // without update_match_index there is a bug when the cursor is before the first match
875                        search_bar.update_match_index(window, cx);
876                        search_bar.select_match(direction.opposite(), 1, window, cx);
877                    });
878                }
879            });
880        }
881        self.update_editor(cx, |_, editor, cx| {
882            let latest = editor
883                .selections
884                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
885            start_selection = latest.start;
886            end_selection = latest.end;
887        });
888
889        let mut match_exists = false;
890        pane.update(cx, |pane, cx| {
891            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
892                search_bar.update(cx, |search_bar, cx| {
893                    search_bar.update_match_index(window, cx);
894                    search_bar.select_match(direction, count, window, cx);
895                    match_exists = search_bar.match_exists(window, cx);
896                });
897            }
898        });
899        if !match_exists {
900            self.update_editor(cx, |_, editor, _| {
901                editor.set_collapse_matches(true);
902            });
903            self.clear_operator(window, cx);
904            self.stop_replaying(cx);
905            return;
906        }
907        self.update_editor(cx, |_, editor, cx| {
908            let latest = editor
909                .selections
910                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
911            if vim_is_normal {
912                start_selection = latest.start;
913                end_selection = latest.end;
914            } else {
915                start_selection = start_selection.min(latest.start);
916                end_selection = end_selection.max(latest.end);
917            }
918            if direction == Direction::Prev {
919                std::mem::swap(&mut start_selection, &mut end_selection);
920            }
921            editor.change_selections(Default::default(), window, cx, |s| {
922                s.select_ranges([start_selection..end_selection]);
923            });
924            editor.set_collapse_matches(true);
925        });
926
927        match self.maybe_pop_operator() {
928            Some(Operator::Change) => self.substitute(None, false, window, cx),
929            Some(Operator::Delete) => {
930                self.stop_recording(cx);
931                self.visual_delete(false, window, cx);
932            }
933            Some(Operator::Yank) => self.visual_yank(false, window, cx),
934            _ => {} // Ignoring other operators
935        }
936    }
937}
938#[cfg(test)]
939mod test {
940    use indoc::indoc;
941    use workspace::item::Item;
942
943    use crate::{
944        state::Mode,
945        test::{NeovimBackedTestContext, VimTestContext},
946    };
947
948    #[gpui::test]
949    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
950        let mut cx = NeovimBackedTestContext::new(cx).await;
951
952        cx.set_shared_state(indoc! {
953            "The ˇquick brown
954            fox jumps over
955            the lazy dog"
956        })
957        .await;
958        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
959
960        // entering visual mode should select the character
961        // under cursor
962        cx.simulate_shared_keystrokes("v").await;
963        cx.shared_state()
964            .await
965            .assert_eq(indoc! { "The «qˇ»uick brown
966            fox jumps over
967            the lazy dog"});
968        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
969
970        // forwards motions should extend the selection
971        cx.simulate_shared_keystrokes("w j").await;
972        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
973            fox jumps oˇ»ver
974            the lazy dog"});
975
976        cx.simulate_shared_keystrokes("escape").await;
977        cx.shared_state().await.assert_eq(indoc! { "The quick brown
978            fox jumps ˇover
979            the lazy dog"});
980
981        // motions work backwards
982        cx.simulate_shared_keystrokes("v k b").await;
983        cx.shared_state()
984            .await
985            .assert_eq(indoc! { "The «ˇquick brown
986            fox jumps o»ver
987            the lazy dog"});
988
989        // works on empty lines
990        cx.set_shared_state(indoc! {"
991            a
992            ˇ
993            b
994            "})
995            .await;
996        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
997        cx.simulate_shared_keystrokes("v").await;
998        cx.shared_state().await.assert_eq(indoc! {"
999            a
1000            «
1001            ˇ»b
1002        "});
1003        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1004
1005        // toggles off again
1006        cx.simulate_shared_keystrokes("v").await;
1007        cx.shared_state().await.assert_eq(indoc! {"
1008            a
1009            ˇ
1010            b
1011            "});
1012
1013        // works at the end of a document
1014        cx.set_shared_state(indoc! {"
1015            a
1016            b
1017            ˇ"})
1018            .await;
1019
1020        cx.simulate_shared_keystrokes("v").await;
1021        cx.shared_state().await.assert_eq(indoc! {"
1022            a
1023            b
1024            ˇ"});
1025    }
1026
1027    #[gpui::test]
1028    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1029        let mut cx = VimTestContext::new(cx, true).await;
1030
1031        cx.set_state(
1032            indoc! {
1033                "«The quick brown
1034                fox jumps over
1035                the lazy dogˇ»"
1036            },
1037            Mode::Visual,
1038        );
1039        cx.simulate_keystrokes("g shift-i");
1040        cx.assert_state(
1041            indoc! {
1042                "ˇThe quick brown
1043                ˇfox jumps over
1044                ˇthe lazy dog"
1045            },
1046            Mode::Insert,
1047        );
1048    }
1049
1050    #[gpui::test]
1051    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1052        let mut cx = VimTestContext::new(cx, true).await;
1053
1054        cx.set_state(
1055            indoc! {
1056                "«The quick brown
1057                fox jumps over
1058                the lazy dogˇ»"
1059            },
1060            Mode::Visual,
1061        );
1062        cx.simulate_keystrokes("g shift-a");
1063        cx.assert_state(
1064            indoc! {
1065                "The quick brownˇ
1066                fox jumps overˇ
1067                the lazy dogˇ"
1068            },
1069            Mode::Insert,
1070        );
1071    }
1072
1073    #[gpui::test]
1074    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
1075        let mut cx = NeovimBackedTestContext::new(cx).await;
1076
1077        cx.set_shared_state(indoc! {
1078            "The ˇquick brown
1079            fox jumps over
1080            the lazy dog"
1081        })
1082        .await;
1083        cx.simulate_shared_keystrokes("shift-v").await;
1084        cx.shared_state()
1085            .await
1086            .assert_eq(indoc! { "The «qˇ»uick brown
1087            fox jumps over
1088            the lazy dog"});
1089        cx.simulate_shared_keystrokes("x").await;
1090        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
1091        the lazy dog"});
1092
1093        // it should work on empty lines
1094        cx.set_shared_state(indoc! {"
1095            a
1096            ˇ
1097            b"})
1098            .await;
1099        cx.simulate_shared_keystrokes("shift-v").await;
1100        cx.shared_state().await.assert_eq(indoc! {"
1101            a
1102            «
1103            ˇ»b"});
1104        cx.simulate_shared_keystrokes("x").await;
1105        cx.shared_state().await.assert_eq(indoc! {"
1106            a
1107            ˇb"});
1108
1109        // it should work at the end of the document
1110        cx.set_shared_state(indoc! {"
1111            a
1112            b
1113            ˇ"})
1114            .await;
1115        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
1116        cx.simulate_shared_keystrokes("shift-v").await;
1117        cx.shared_state().await.assert_eq(indoc! {"
1118            a
1119            b
1120            ˇ"});
1121        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1122        cx.simulate_shared_keystrokes("x").await;
1123        cx.shared_state().await.assert_eq(indoc! {"
1124            a
1125            ˇb"});
1126    }
1127
1128    #[gpui::test]
1129    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
1130        let mut cx = NeovimBackedTestContext::new(cx).await;
1131
1132        cx.simulate("v w", "The quick ˇbrown")
1133            .await
1134            .assert_matches();
1135
1136        cx.simulate("v w x", "The quick ˇbrown")
1137            .await
1138            .assert_matches();
1139        cx.simulate(
1140            "v w j x",
1141            indoc! {"
1142                The ˇquick brown
1143                fox jumps over
1144                the lazy dog"},
1145        )
1146        .await
1147        .assert_matches();
1148        // Test pasting code copied on delete
1149        cx.simulate_shared_keystrokes("j p").await;
1150        cx.shared_state().await.assert_matches();
1151
1152        cx.simulate_at_each_offset(
1153            "v w j x",
1154            indoc! {"
1155                The ˇquick brown
1156                fox jumps over
1157                the ˇlazy dog"},
1158        )
1159        .await
1160        .assert_matches();
1161        cx.simulate_at_each_offset(
1162            "v b k x",
1163            indoc! {"
1164                The ˇquick brown
1165                fox jumps ˇover
1166                the ˇlazy dog"},
1167        )
1168        .await
1169        .assert_matches();
1170    }
1171
1172    #[gpui::test]
1173    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1174        let mut cx = NeovimBackedTestContext::new(cx).await;
1175
1176        cx.set_shared_state(indoc! {"
1177                The quˇick brown
1178                fox jumps over
1179                the lazy dog"})
1180            .await;
1181        cx.simulate_shared_keystrokes("shift-v x").await;
1182        cx.shared_state().await.assert_matches();
1183
1184        // Test pasting code copied on delete
1185        cx.simulate_shared_keystrokes("p").await;
1186        cx.shared_state().await.assert_matches();
1187
1188        cx.set_shared_state(indoc! {"
1189                The quick brown
1190                fox jumps over
1191                the laˇzy dog"})
1192            .await;
1193        cx.simulate_shared_keystrokes("shift-v x").await;
1194        cx.shared_state().await.assert_matches();
1195        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1196
1197        cx.set_shared_state(indoc! {"
1198                                The quˇick brown
1199                                fox jumps over
1200                                the lazy dog"})
1201            .await;
1202        cx.simulate_shared_keystrokes("shift-v j x").await;
1203        cx.shared_state().await.assert_matches();
1204        // Test pasting code copied on delete
1205        cx.simulate_shared_keystrokes("p").await;
1206        cx.shared_state().await.assert_matches();
1207
1208        cx.set_shared_state(indoc! {"
1209            The ˇlong line
1210            should not
1211            crash
1212            "})
1213            .await;
1214        cx.simulate_shared_keystrokes("shift-v $ x").await;
1215        cx.shared_state().await.assert_matches();
1216    }
1217
1218    #[gpui::test]
1219    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1220        let mut cx = NeovimBackedTestContext::new(cx).await;
1221
1222        cx.set_shared_state("The quick ˇbrown").await;
1223        cx.simulate_shared_keystrokes("v w y").await;
1224        cx.shared_state().await.assert_eq("The quick ˇbrown");
1225        cx.shared_clipboard().await.assert_eq("brown");
1226
1227        cx.set_shared_state(indoc! {"
1228                The ˇquick brown
1229                fox jumps over
1230                the lazy dog"})
1231            .await;
1232        cx.simulate_shared_keystrokes("v w j y").await;
1233        cx.shared_state().await.assert_eq(indoc! {"
1234                    The ˇquick brown
1235                    fox jumps over
1236                    the lazy dog"});
1237        cx.shared_clipboard().await.assert_eq(indoc! {"
1238                quick brown
1239                fox jumps o"});
1240
1241        cx.set_shared_state(indoc! {"
1242                    The quick brown
1243                    fox jumps over
1244                    the ˇlazy dog"})
1245            .await;
1246        cx.simulate_shared_keystrokes("v w j y").await;
1247        cx.shared_state().await.assert_eq(indoc! {"
1248                    The quick brown
1249                    fox jumps over
1250                    the ˇlazy dog"});
1251        cx.shared_clipboard().await.assert_eq("lazy d");
1252        cx.simulate_shared_keystrokes("shift-v y").await;
1253        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1254
1255        cx.set_shared_state(indoc! {"
1256                    The ˇquick brown
1257                    fox jumps over
1258                    the lazy dog"})
1259            .await;
1260        cx.simulate_shared_keystrokes("v b k y").await;
1261        cx.shared_state().await.assert_eq(indoc! {"
1262                    ˇThe quick brown
1263                    fox jumps over
1264                    the lazy dog"});
1265        assert_eq!(
1266            cx.read_from_clipboard()
1267                .map(|item| item.text().unwrap())
1268                .unwrap(),
1269            "The q"
1270        );
1271
1272        cx.set_shared_state(indoc! {"
1273                    The quick brown
1274                    fox ˇjumps over
1275                    the lazy dog"})
1276            .await;
1277        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1278            .await;
1279        cx.shared_state().await.assert_eq(indoc! {"
1280                    The quick brown
1281                    ˇfox jumps over
1282                    the lazy dog"});
1283        cx.shared_clipboard()
1284            .await
1285            .assert_eq("fox jumps over\nthe lazy dog\n");
1286
1287        cx.set_shared_state(indoc! {"
1288                    The quick brown
1289                    fox ˇjumps over
1290                    the lazy dog"})
1291            .await;
1292        cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
1293        cx.shared_state().await.assert_eq(indoc! {"
1294                    The quick brown
1295                    ˇfox jumps over
1296                    the lazy dog"});
1297        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
1298    }
1299
1300    #[gpui::test]
1301    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1302        let mut cx = NeovimBackedTestContext::new(cx).await;
1303
1304        cx.set_shared_state(indoc! {
1305            "The ˇquick brown
1306             fox jumps over
1307             the lazy dog"
1308        })
1309        .await;
1310        cx.simulate_shared_keystrokes("ctrl-v").await;
1311        cx.shared_state().await.assert_eq(indoc! {
1312            "The «qˇ»uick brown
1313            fox jumps over
1314            the lazy dog"
1315        });
1316        cx.simulate_shared_keystrokes("2 down").await;
1317        cx.shared_state().await.assert_eq(indoc! {
1318            "The «qˇ»uick brown
1319            fox «jˇ»umps over
1320            the «lˇ»azy dog"
1321        });
1322        cx.simulate_shared_keystrokes("e").await;
1323        cx.shared_state().await.assert_eq(indoc! {
1324            "The «quicˇ»k brown
1325            fox «jumpˇ»s over
1326            the «lazyˇ» dog"
1327        });
1328        cx.simulate_shared_keystrokes("^").await;
1329        cx.shared_state().await.assert_eq(indoc! {
1330            "«ˇThe q»uick brown
1331            «ˇfox j»umps over
1332            «ˇthe l»azy dog"
1333        });
1334        cx.simulate_shared_keystrokes("$").await;
1335        cx.shared_state().await.assert_eq(indoc! {
1336            "The «quick brownˇ»
1337            fox «jumps overˇ»
1338            the «lazy dogˇ»"
1339        });
1340        cx.simulate_shared_keystrokes("shift-f space").await;
1341        cx.shared_state().await.assert_eq(indoc! {
1342            "The «quickˇ» brown
1343            fox «jumpsˇ» over
1344            the «lazy ˇ»dog"
1345        });
1346
1347        // toggling through visual mode works as expected
1348        cx.simulate_shared_keystrokes("v").await;
1349        cx.shared_state().await.assert_eq(indoc! {
1350            "The «quick brown
1351            fox jumps over
1352            the lazy ˇ»dog"
1353        });
1354        cx.simulate_shared_keystrokes("ctrl-v").await;
1355        cx.shared_state().await.assert_eq(indoc! {
1356            "The «quickˇ» brown
1357            fox «jumpsˇ» over
1358            the «lazy ˇ»dog"
1359        });
1360
1361        cx.set_shared_state(indoc! {
1362            "The ˇquick
1363             brown
1364             fox
1365             jumps over the
1366
1367             lazy dog
1368            "
1369        })
1370        .await;
1371        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1372        cx.shared_state().await.assert_eq(indoc! {
1373            "The«ˇ q»uick
1374            bro«ˇwn»
1375            foxˇ
1376            jumps over the
1377
1378            lazy dog
1379            "
1380        });
1381        cx.simulate_shared_keystrokes("down").await;
1382        cx.shared_state().await.assert_eq(indoc! {
1383            "The «qˇ»uick
1384            brow«nˇ»
1385            fox
1386            jump«sˇ» over the
1387
1388            lazy dog
1389            "
1390        });
1391        cx.simulate_shared_keystrokes("left").await;
1392        cx.shared_state().await.assert_eq(indoc! {
1393            "The«ˇ q»uick
1394            bro«ˇwn»
1395            foxˇ
1396            jum«ˇps» over the
1397
1398            lazy dog
1399            "
1400        });
1401        cx.simulate_shared_keystrokes("s o escape").await;
1402        cx.shared_state().await.assert_eq(indoc! {
1403            "Theˇouick
1404            broo
1405            foxo
1406            jumo over the
1407
1408            lazy dog
1409            "
1410        });
1411
1412        // https://github.com/zed-industries/zed/issues/6274
1413        cx.set_shared_state(indoc! {
1414            "Theˇ quick brown
1415
1416            fox jumps over
1417            the lazy dog
1418            "
1419        })
1420        .await;
1421        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1422        cx.shared_state().await.assert_eq(indoc! {
1423            "The «qˇ»uick brown
1424
1425            fox «jˇ»umps over
1426            the lazy dog
1427            "
1428        });
1429    }
1430
1431    #[gpui::test]
1432    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1433        let mut cx = NeovimBackedTestContext::new(cx).await;
1434
1435        cx.set_shared_state(indoc! {
1436            "The ˇquick brown
1437            fox jumps over
1438            the lazy dog
1439            "
1440        })
1441        .await;
1442        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1443        cx.shared_state().await.assert_eq(indoc! {
1444            "The «quˇ»ick brown
1445            fox «juˇ»mps over
1446            the lazy dog
1447            "
1448        });
1449    }
1450    #[gpui::test]
1451    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1452        let mut cx = NeovimBackedTestContext::new(cx).await;
1453        cx.set_shared_state(indoc! {"
1454            The ˇquick brown
1455            fox jumps over
1456            the lazy dog"})
1457            .await;
1458        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1459        cx.shared_state().await.assert_eq(indoc! {"
1460            The «quick ˇ»brown
1461            fox «jumps ˇ»over
1462            the lazy dog"});
1463    }
1464
1465    #[gpui::test]
1466    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1467        let mut cx = NeovimBackedTestContext::new(cx).await;
1468        cx.set_shared_state(indoc! {"
1469            The quick brown
1470            fox jumpsˇ over
1471            the lazy dog"})
1472            .await;
1473        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1474        cx.shared_state().await.assert_eq(indoc! {"
1475            The «ˇquick »brown
1476            fox «ˇjumps »over
1477            the lazy dog"});
1478    }
1479
1480    #[gpui::test]
1481    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1482        let mut cx = NeovimBackedTestContext::new(cx).await;
1483        cx.set_shared_state(indoc! {"
1484            The quick brown
1485            fox jˇumps over
1486            the lazy dog"})
1487            .await;
1488        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1489        cx.shared_state().await.assert_eq(indoc! {"
1490            The quick brown
1491            fox j«umps ˇ»over
1492            the l«azy dˇ»og"});
1493        cx.simulate_shared_keystrokes("o k").await;
1494        cx.shared_state().await.assert_eq(indoc! {"
1495            The q«ˇuick »brown
1496            fox j«ˇumps »over
1497            the l«ˇazy d»og"});
1498    }
1499
1500    #[gpui::test]
1501    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1502        let mut cx = NeovimBackedTestContext::new(cx).await;
1503        cx.set_shared_state(indoc! {"
1504            The quick brown
1505            fox jˇumps over
1506            the lazy dog"})
1507            .await;
1508        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1509        cx.shared_state().await.assert_eq(indoc! {"
1510            The quick brown
1511            fox j«umps ˇ»over
1512            the l«azy dˇ»og"});
1513        cx.simulate_shared_keystrokes("shift-o k").await;
1514        cx.shared_state().await.assert_eq(indoc! {"
1515            The quick brown
1516            fox j«ˇumps »over
1517            the lazy dog"});
1518    }
1519
1520    #[gpui::test]
1521    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1522        let mut cx = NeovimBackedTestContext::new(cx).await;
1523
1524        cx.set_shared_state(indoc! {
1525            "ˇThe quick brown
1526            fox jumps over
1527            the lazy dog
1528            "
1529        })
1530        .await;
1531        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1532        cx.shared_state().await.assert_eq(indoc! {
1533            "«Tˇ»he quick brown
1534            «fˇ»ox jumps over
1535            «tˇ»he lazy dog
1536            ˇ"
1537        });
1538
1539        cx.simulate_shared_keystrokes("shift-i k escape").await;
1540        cx.shared_state().await.assert_eq(indoc! {
1541            "ˇkThe quick brown
1542            kfox jumps over
1543            kthe lazy dog
1544            k"
1545        });
1546
1547        cx.set_shared_state(indoc! {
1548            "ˇThe quick brown
1549            fox jumps over
1550            the lazy dog
1551            "
1552        })
1553        .await;
1554        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1555        cx.shared_state().await.assert_eq(indoc! {
1556            "«Tˇ»he quick brown
1557            «fˇ»ox jumps over
1558            «tˇ»he lazy dog
1559            ˇ"
1560        });
1561        cx.simulate_shared_keystrokes("c k escape").await;
1562        cx.shared_state().await.assert_eq(indoc! {
1563            "ˇkhe quick brown
1564            kox jumps over
1565            khe lazy dog
1566            k"
1567        });
1568    }
1569
1570    #[gpui::test]
1571    async fn test_visual_block_insert_after_ctrl_d_scroll(cx: &mut gpui::TestAppContext) {
1572        let mut cx = NeovimBackedTestContext::new(cx).await;
1573        let shared_state_lines = (1..=10)
1574            .map(|line_number| format!("{line_number:02}"))
1575            .collect::<Vec<_>>()
1576            .join("\n");
1577        let shared_state = format!("ˇ{shared_state_lines}\n");
1578
1579        cx.set_scroll_height(5).await;
1580        cx.set_shared_state(&shared_state).await;
1581
1582        cx.simulate_shared_keystrokes("ctrl-v ctrl-d").await;
1583        cx.shared_state().await.assert_matches();
1584
1585        cx.simulate_shared_keystrokes("shift-i x escape").await;
1586        cx.shared_state().await.assert_eq(indoc! {
1587            "
1588            ˇx01
1589            x02
1590            x03
1591            x04
1592            x05
1593            06
1594            07
1595            08
1596            09
1597            10
1598            "
1599        });
1600    }
1601
1602    #[gpui::test]
1603    async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) {
1604        let mut cx = NeovimBackedTestContext::new(cx).await;
1605
1606        // Ensure that the editor is wrapping lines at 12 columns so that each
1607        // of the lines ends up being wrapped.
1608        cx.set_shared_wrap(12).await;
1609        cx.set_shared_state(indoc! {
1610            "ˇ12345678901234567890
1611            12345678901234567890
1612            12345678901234567890
1613            "
1614        })
1615        .await;
1616        cx.simulate_shared_keystrokes("ctrl-v j").await;
1617        cx.shared_state().await.assert_eq(indoc! {
1618            "«1ˇ»2345678901234567890
1619            «1ˇ»2345678901234567890
1620            12345678901234567890
1621            "
1622        });
1623
1624        // Test with lines taking up different amounts of display rows to ensure
1625        // that, even in that case, only the buffer rows are taken into account.
1626        cx.set_shared_state(indoc! {
1627            "ˇ123456789012345678901234567890123456789012345678901234567890
1628            1234567890123456789012345678901234567890
1629            12345678901234567890
1630            "
1631        })
1632        .await;
1633        cx.simulate_shared_keystrokes("ctrl-v 2 j").await;
1634        cx.shared_state().await.assert_eq(indoc! {
1635            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1636            «1ˇ»234567890123456789012345678901234567890
1637            «1ˇ»2345678901234567890
1638            "
1639        });
1640
1641        // Same scenario as above, but using the up motion to ensure that the
1642        // result is the same.
1643        cx.set_shared_state(indoc! {
1644            "123456789012345678901234567890123456789012345678901234567890
1645            1234567890123456789012345678901234567890
1646            ˇ12345678901234567890
1647            "
1648        })
1649        .await;
1650        cx.simulate_shared_keystrokes("ctrl-v 2 k").await;
1651        cx.shared_state().await.assert_eq(indoc! {
1652            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1653            «1ˇ»234567890123456789012345678901234567890
1654            «1ˇ»2345678901234567890
1655            "
1656        });
1657    }
1658
1659    #[gpui::test]
1660    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1661        let mut cx = NeovimBackedTestContext::new(cx).await;
1662
1663        cx.set_shared_state("hello (in [parˇens] o)").await;
1664        cx.simulate_shared_keystrokes("ctrl-v l").await;
1665        cx.simulate_shared_keystrokes("a ]").await;
1666        cx.shared_state()
1667            .await
1668            .assert_eq("hello (in «[parens]ˇ» o)");
1669        cx.simulate_shared_keystrokes("i (").await;
1670        cx.shared_state()
1671            .await
1672            .assert_eq("hello («in [parens] oˇ»)");
1673
1674        cx.set_shared_state("hello in a wˇord again.").await;
1675        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1676        cx.shared_state()
1677            .await
1678            .assert_eq("hello in a w«ordˇ» again.");
1679        assert_eq!(cx.mode(), Mode::VisualBlock);
1680        cx.simulate_shared_keystrokes("o a s").await;
1681        cx.shared_state()
1682            .await
1683            .assert_eq("«ˇhello in a word» again.");
1684    }
1685
1686    #[gpui::test]
1687    async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1688        let mut cx = NeovimBackedTestContext::new(cx).await;
1689
1690        cx.set_shared_state(indoc! {
1691            "{
1692                {
1693               ˇ }
1694            }
1695            {
1696            }
1697            "
1698        })
1699        .await;
1700        cx.simulate_shared_keystrokes("v l").await;
1701        cx.shared_state().await.assert_eq(indoc! {
1702            "{
1703                {
1704               « }ˇ»
1705            }
1706            {
1707            }
1708            "
1709        });
1710        cx.simulate_shared_keystrokes("a {").await;
1711        cx.shared_state().await.assert_eq(indoc! {
1712            "{
1713                «{
1714                }ˇ»
1715            }
1716            {
1717            }
1718            "
1719        });
1720        cx.simulate_shared_keystrokes("a {").await;
1721        cx.shared_state().await.assert_eq(indoc! {
1722            "«{
1723                {
1724                }
1725            }ˇ»
1726            {
1727            }
1728            "
1729        });
1730        // cx.simulate_shared_keystrokes("a {").await;
1731        // cx.shared_state().await.assert_eq(indoc! {
1732        //     "{
1733        //         «{
1734        //         }ˇ»
1735        //     }
1736        //     {
1737        //     }
1738        //     "
1739        // });
1740    }
1741
1742    #[gpui::test]
1743    async fn test_visual_move_trailing_newline(cx: &mut gpui::TestAppContext) {
1744        let mut cx = VimTestContext::new(cx, true).await;
1745
1746        cx.set_state("This is a newlinˇe\n", Mode::Normal);
1747        cx.simulate_keystrokes("v");
1748        cx.assert_state("This is a newlin«eˇ»\n", Mode::Visual);
1749        cx.simulate_keystrokes("l");
1750        cx.assert_state("This is a newlin«e\nˇ»", Mode::Visual);
1751    }
1752
1753    #[gpui::test]
1754    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1755        let mut cx = VimTestContext::new(cx, true).await;
1756
1757        cx.set_state("aˇbc", Mode::Normal);
1758        cx.simulate_keystrokes("ctrl-v");
1759        assert_eq!(cx.mode(), Mode::VisualBlock);
1760        cx.simulate_keystrokes("cmd-shift-p escape");
1761        assert_eq!(cx.mode(), Mode::VisualBlock);
1762    }
1763
1764    #[gpui::test]
1765    async fn test_gn(cx: &mut gpui::TestAppContext) {
1766        let mut cx = NeovimBackedTestContext::new(cx).await;
1767
1768        cx.set_shared_state("aaˇ aa aa aa aa").await;
1769        cx.simulate_shared_keystrokes("/ a a enter").await;
1770        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1771        cx.simulate_shared_keystrokes("g n").await;
1772        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1773        cx.simulate_shared_keystrokes("g n").await;
1774        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1775        cx.simulate_shared_keystrokes("escape d g n").await;
1776        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1777
1778        cx.set_shared_state("aaˇ aa aa aa aa").await;
1779        cx.simulate_shared_keystrokes("/ a a enter").await;
1780        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1781        cx.simulate_shared_keystrokes("3 g n").await;
1782        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1783
1784        cx.set_shared_state("aaˇ aa aa aa aa").await;
1785        cx.simulate_shared_keystrokes("/ a a enter").await;
1786        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1787        cx.simulate_shared_keystrokes("g shift-n").await;
1788        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1789        cx.simulate_shared_keystrokes("g shift-n").await;
1790        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1791    }
1792
1793    #[gpui::test]
1794    async fn test_gl(cx: &mut gpui::TestAppContext) {
1795        let mut cx = VimTestContext::new(cx, true).await;
1796
1797        cx.set_state("aaˇ aa\naa", Mode::Normal);
1798        cx.simulate_keystrokes("g l");
1799        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1800        cx.simulate_keystrokes("g >");
1801        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1802    }
1803
1804    #[gpui::test]
1805    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1806        let mut cx = NeovimBackedTestContext::new(cx).await;
1807
1808        cx.set_shared_state("aaˇ aa aa aa aa").await;
1809        cx.simulate_shared_keystrokes("/ a a enter").await;
1810        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1811        cx.simulate_shared_keystrokes("d g n").await;
1812
1813        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1814        cx.simulate_shared_keystrokes(".").await;
1815        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1816        cx.simulate_shared_keystrokes(".").await;
1817        cx.shared_state().await.assert_eq("aa   ˇ aa");
1818    }
1819
1820    #[gpui::test]
1821    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1822        let mut cx = NeovimBackedTestContext::new(cx).await;
1823
1824        cx.set_shared_state("aaˇ aa aa aa aa").await;
1825        cx.simulate_shared_keystrokes("/ a a enter").await;
1826        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1827        cx.simulate_shared_keystrokes("c g n x escape").await;
1828        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1829        cx.simulate_shared_keystrokes(".").await;
1830        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1831    }
1832
1833    #[gpui::test]
1834    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1835        let mut cx = NeovimBackedTestContext::new(cx).await;
1836
1837        cx.set_shared_state("aaˇ aa aa aa aa").await;
1838        cx.simulate_shared_keystrokes("/ b b enter").await;
1839        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1840        cx.simulate_shared_keystrokes("c g n x escape").await;
1841        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1842        cx.simulate_shared_keystrokes(".").await;
1843        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1844
1845        cx.set_shared_state("aaˇ bb aa aa aa").await;
1846        cx.simulate_shared_keystrokes("/ b b enter").await;
1847        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1848        cx.simulate_shared_keystrokes("c g n x escape").await;
1849        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1850        cx.simulate_shared_keystrokes(".").await;
1851        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1852    }
1853
1854    #[gpui::test]
1855    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1856        let mut cx = NeovimBackedTestContext::new(cx).await;
1857
1858        cx.set_shared_state(indoc! {
1859            "The ˇquick brown
1860            fox jumps over
1861            the lazy dog
1862            "
1863        })
1864        .await;
1865        cx.simulate_shared_keystrokes("v down shift-d").await;
1866        cx.shared_state().await.assert_eq(indoc! {
1867            "the ˇlazy dog\n"
1868        });
1869
1870        cx.set_shared_state(indoc! {
1871            "The ˇquick brown
1872            fox jumps over
1873            the lazy dog
1874            "
1875        })
1876        .await;
1877        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1878        cx.shared_state().await.assert_eq(indoc! {
1879            "Theˇ•
1880            fox•
1881            the lazy dog
1882            "
1883        });
1884    }
1885
1886    #[gpui::test]
1887    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1888        let mut cx = NeovimBackedTestContext::new(cx).await;
1889
1890        cx.set_shared_state(indoc! {
1891            "The ˇquick brown\n"
1892        })
1893        .await;
1894        cx.simulate_shared_keystrokes("v i w shift-y").await;
1895        cx.shared_clipboard().await.assert_eq(indoc! {
1896            "The quick brown\n"
1897        });
1898    }
1899
1900    #[gpui::test]
1901    async fn test_gv(cx: &mut gpui::TestAppContext) {
1902        let mut cx = NeovimBackedTestContext::new(cx).await;
1903
1904        cx.set_shared_state(indoc! {
1905            "The ˇquick brown"
1906        })
1907        .await;
1908        cx.simulate_shared_keystrokes("v i w escape g v").await;
1909        cx.shared_state().await.assert_eq(indoc! {
1910            "The «quickˇ» brown"
1911        });
1912
1913        cx.simulate_shared_keystrokes("o escape g v").await;
1914        cx.shared_state().await.assert_eq(indoc! {
1915            "The «ˇquick» brown"
1916        });
1917
1918        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1919        cx.shared_state().await.assert_eq(indoc! {
1920            "«Thˇ»e quick brown"
1921        });
1922        cx.simulate_shared_keystrokes("g v").await;
1923        cx.shared_state().await.assert_eq(indoc! {
1924            "The «ˇquick» brown"
1925        });
1926        cx.simulate_shared_keystrokes("g v").await;
1927        cx.shared_state().await.assert_eq(indoc! {
1928            "«Thˇ»e quick brown"
1929        });
1930
1931        cx.set_state(
1932            indoc! {"
1933            fiˇsh one
1934            fish two
1935            fish red
1936            fish blue
1937        "},
1938            Mode::Normal,
1939        );
1940        cx.simulate_keystrokes("4 g l escape escape g v");
1941        cx.assert_state(
1942            indoc! {"
1943                «fishˇ» one
1944                «fishˇ» two
1945                «fishˇ» red
1946                «fishˇ» blue
1947            "},
1948            Mode::Visual,
1949        );
1950        cx.simulate_keystrokes("y g v");
1951        cx.assert_state(
1952            indoc! {"
1953                «fishˇ» one
1954                «fishˇ» two
1955                «fishˇ» red
1956                «fishˇ» blue
1957            "},
1958            Mode::Visual,
1959        );
1960    }
1961
1962    #[gpui::test]
1963    async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) {
1964        let mut cx = NeovimBackedTestContext::new(cx).await;
1965
1966        cx.set_shared_state(indoc! {
1967            "The
1968            quicˇk
1969            brown
1970            fox"
1971        })
1972        .await;
1973        cx.simulate_shared_keystrokes("y y j shift-v p g v y").await;
1974        cx.shared_state().await.assert_eq(indoc! {
1975            "The
1976            quick
1977            ˇquick
1978            fox"
1979        });
1980        cx.shared_clipboard().await.assert_eq("quick\n");
1981    }
1982
1983    #[gpui::test]
1984    async fn test_v2ap(cx: &mut gpui::TestAppContext) {
1985        let mut cx = NeovimBackedTestContext::new(cx).await;
1986
1987        cx.set_shared_state(indoc! {
1988            "The
1989            quicˇk
1990
1991            brown
1992            fox"
1993        })
1994        .await;
1995        cx.simulate_shared_keystrokes("v 2 a p").await;
1996        cx.shared_state().await.assert_eq(indoc! {
1997            "«The
1998            quick
1999
2000            brown
2001            fˇ»ox"
2002        });
2003    }
2004
2005    #[gpui::test]
2006    async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) {
2007        let mut cx = VimTestContext::new(cx, true).await;
2008
2009        cx.set_state(
2010            indoc! {"
2011                fn test() {
2012                    let ˇa = 1;
2013                    let b = 2;
2014                    let c = 3;
2015                }
2016            "},
2017            Mode::Normal,
2018        );
2019
2020        // Enter visual mode and select the statement
2021        cx.simulate_keystrokes("v w w w");
2022        cx.assert_state(
2023            indoc! {"
2024                fn test() {
2025                    let «a = 1;ˇ»
2026                    let b = 2;
2027                    let c = 3;
2028                }
2029            "},
2030            Mode::Visual,
2031        );
2032
2033        // The specific behavior of syntax sibling selection in vim mode
2034        // would depend on the key bindings configured, but the actions
2035        // are now available for use
2036    }
2037
2038    #[gpui::test]
2039    async fn test_visual_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2040        let mut cx = VimTestContext::new(cx, true).await;
2041
2042        cx.set_state("«Hällöˇ» Wörld", Mode::Visual);
2043        cx.simulate_keystrokes("r 1");
2044        cx.assert_state("ˇ11111 Wörld", Mode::Normal);
2045
2046        cx.set_state("«e\u{301}ˇ»", Mode::Visual);
2047        cx.simulate_keystrokes("r 1");
2048        cx.assert_state("ˇ1", Mode::Normal);
2049
2050        cx.set_state("«🙂ˇ»", Mode::Visual);
2051        cx.simulate_keystrokes("r 1");
2052        cx.assert_state("ˇ1", Mode::Normal);
2053    }
2054}
2055
Served at tenant.openagents/omega Member data and write actions are omitted.