visual.rs

  1use anyhow::Result;
  2use std::{cmp, sync::Arc};
  3
  4use collections::HashMap;
  5use editor::{
  6    display_map::{DisplaySnapshot, ToDisplayPoint},
  7    movement,
  8    scroll::autoscroll::Autoscroll,
  9    Bias, DisplayPoint, Editor,
 10};
 11use gpui::{actions, AppContext, ViewContext, WindowContext};
 12use language::{Selection, SelectionGoal};
 13use workspace::Workspace;
 14
 15use crate::{
 16    motion::{start_of_line, Motion},
 17    object::Object,
 18    state::{Mode, Operator},
 19    utils::copy_selections_content,
 20    Vim,
 21};
 22
 23actions!(
 24    vim,
 25    [
 26        ToggleVisual,
 27        ToggleVisualLine,
 28        ToggleVisualBlock,
 29        VisualDelete,
 30        VisualYank,
 31        OtherEnd,
 32        SelectNext,
 33        SelectPrevious,
 34    ]
 35);
 36
 37pub fn init(cx: &mut AppContext) {
 38    cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
 39        toggle_mode(Mode::Visual, cx)
 40    });
 41    cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
 42        toggle_mode(Mode::VisualLine, cx)
 43    });
 44    cx.add_action(
 45        |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
 46            toggle_mode(Mode::VisualBlock, cx)
 47        },
 48    );
 49    cx.add_action(other_end);
 50    cx.add_action(delete);
 51    cx.add_action(yank);
 52
 53    cx.add_action(select_next);
 54    cx.add_action(select_previous);
 55}
 56
 57pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
 58    Vim::update(cx, |vim, cx| {
 59        vim.update_active_editor(cx, |editor, cx| {
 60            if vim.state().mode == Mode::VisualBlock
 61                && !matches!(
 62                    motion,
 63                    Motion::EndOfLine {
 64                        display_lines: false
 65                    }
 66                )
 67            {
 68                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
 69                visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
 70                    motion.move_point(map, point, goal, times)
 71                })
 72            } else {
 73                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 74                    s.move_with(|map, selection| {
 75                        let was_reversed = selection.reversed;
 76                        let mut current_head = selection.head();
 77
 78                        // our motions assume the current character is after the cursor,
 79                        // but in (forward) visual mode the current character is just
 80                        // before the end of the selection.
 81
 82                        // If the file ends with a newline (which is common) we don't do this.
 83                        // so that if you go to the end of such a file you can use "up" to go
 84                        // to the previous line and have it work somewhat as expected.
 85                        if !selection.reversed
 86                            && !selection.is_empty()
 87                            && !(selection.end.column() == 0 && selection.end == map.max_point())
 88                        {
 89                            current_head = movement::left(map, selection.end)
 90                        }
 91
 92                        let Some((new_head, goal)) =
 93                            motion.move_point(map, current_head, selection.goal, times)
 94                        else {
 95                            return;
 96                        };
 97
 98                        selection.set_head(new_head, goal);
 99
100                        // ensure the current character is included in the selection.
101                        if !selection.reversed {
102                            let next_point = if vim.state().mode == Mode::VisualBlock {
103                                movement::saturating_right(map, selection.end)
104                            } else {
105                                movement::right(map, selection.end)
106                            };
107
108                            if !(next_point.column() == 0 && next_point == map.max_point()) {
109                                selection.end = next_point;
110                            }
111                        }
112
113                        // vim always ensures the anchor character stays selected.
114                        // if our selection has reversed, we need to move the opposite end
115                        // to ensure the anchor is still selected.
116                        if was_reversed && !selection.reversed {
117                            selection.start = movement::left(map, selection.start);
118                        } else if !was_reversed && selection.reversed {
119                            selection.end = movement::right(map, selection.end);
120                        }
121                    })
122                });
123            }
124        });
125    });
126}
127
128pub fn visual_block_motion(
129    preserve_goal: bool,
130    editor: &mut Editor,
131    cx: &mut ViewContext<Editor>,
132    mut move_selection: impl FnMut(
133        &DisplaySnapshot,
134        DisplayPoint,
135        SelectionGoal,
136    ) -> Option<(DisplayPoint, SelectionGoal)>,
137) {
138    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
139        let map = &s.display_map();
140        let mut head = s.newest_anchor().head().to_display_point(map);
141        let mut tail = s.oldest_anchor().tail().to_display_point(map);
142
143        let (start, end) = match s.newest_anchor().goal {
144            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
145            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
146            _ => (tail.column(), head.column()),
147        };
148        let goal = SelectionGoal::ColumnRange { start, end };
149
150        let was_reversed = tail.column() > head.column();
151        if !was_reversed && !preserve_goal {
152            head = movement::saturating_left(map, head);
153        }
154
155        let Some((new_head, _)) = move_selection(&map, head, goal) else {
156            return;
157        };
158        head = new_head;
159
160        let is_reversed = tail.column() > head.column();
161        if was_reversed && !is_reversed {
162            tail = movement::left(map, tail)
163        } else if !was_reversed && is_reversed {
164            tail = movement::right(map, tail)
165        }
166        if !is_reversed && !preserve_goal {
167            head = movement::saturating_right(map, head)
168        }
169
170        let columns = if is_reversed {
171            head.column()..tail.column()
172        } else if head.column() == tail.column() {
173            head.column()..(head.column() + 1)
174        } else {
175            tail.column()..head.column()
176        };
177
178        let mut selections = Vec::new();
179        let mut row = tail.row();
180
181        loop {
182            let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
183            let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
184            if columns.start <= map.line_len(row) {
185                let selection = Selection {
186                    id: s.new_selection_id(),
187                    start: start.to_point(map),
188                    end: end.to_point(map),
189                    reversed: is_reversed,
190                    goal: goal.clone(),
191                };
192
193                selections.push(selection);
194            }
195            if row == head.row() {
196                break;
197            }
198            if tail.row() > head.row() {
199                row -= 1
200            } else {
201                row += 1
202            }
203        }
204
205        s.select(selections);
206    })
207}
208
209pub fn visual_object(object: Object, cx: &mut WindowContext) {
210    Vim::update(cx, |vim, cx| {
211        if let Some(Operator::Object { around }) = vim.active_operator() {
212            vim.pop_operator(cx);
213            let current_mode = vim.state().mode;
214            let target_mode = object.target_visual_mode(current_mode);
215            if target_mode != current_mode {
216                vim.switch_mode(target_mode, true, cx);
217            }
218
219            vim.update_active_editor(cx, |editor, cx| {
220                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
221                    s.move_with(|map, selection| {
222                        let mut head = selection.head();
223
224                        // all our motions assume that the current character is
225                        // after the cursor; however in the case of a visual selection
226                        // the current character is before the cursor.
227                        if !selection.reversed {
228                            head = movement::left(map, head);
229                        }
230
231                        if let Some(range) = object.range(map, head, around) {
232                            if !range.is_empty() {
233                                let expand_both_ways =
234                                    if object.always_expands_both_ways() || selection.is_empty() {
235                                        true
236                                        // contains only one character
237                                    } else if let Some((_, start)) =
238                                        map.reverse_chars_at(selection.end).next()
239                                    {
240                                        selection.start == start
241                                    } else {
242                                        false
243                                    };
244
245                                if expand_both_ways {
246                                    selection.start = cmp::min(selection.start, range.start);
247                                    selection.end = cmp::max(selection.end, range.end);
248                                } else if selection.reversed {
249                                    selection.start = range.start;
250                                } else {
251                                    selection.end = range.end;
252                                }
253                            }
254                        }
255                    });
256                });
257            });
258        }
259    });
260}
261
262fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
263    Vim::update(cx, |vim, cx| {
264        if vim.state().mode == mode {
265            vim.switch_mode(Mode::Normal, false, cx);
266        } else {
267            vim.switch_mode(mode, false, cx);
268        }
269    })
270}
271
272pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
273    Vim::update(cx, |vim, cx| {
274        vim.update_active_editor(cx, |editor, cx| {
275            editor.change_selections(None, cx, |s| {
276                s.move_with(|_, selection| {
277                    selection.reversed = !selection.reversed;
278                })
279            })
280        })
281    });
282}
283
284pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
285    Vim::update(cx, |vim, cx| {
286        vim.record_current_action(cx);
287        vim.update_active_editor(cx, |editor, cx| {
288            let mut original_columns: HashMap<_, _> = Default::default();
289            let line_mode = editor.selections.line_mode;
290
291            editor.transact(cx, |editor, cx| {
292                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
293                    s.move_with(|map, selection| {
294                        if line_mode {
295                            let mut position = selection.head();
296                            if !selection.reversed {
297                                position = movement::left(map, position);
298                            }
299                            original_columns.insert(selection.id, position.to_point(map).column);
300                        }
301                        selection.goal = SelectionGoal::None;
302                    });
303                });
304                copy_selections_content(editor, line_mode, cx);
305                editor.insert("", cx);
306
307                // Fixup cursor position after the deletion
308                editor.set_clip_at_line_ends(true, cx);
309                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
310                    s.move_with(|map, selection| {
311                        let mut cursor = selection.head().to_point(map);
312
313                        if let Some(column) = original_columns.get(&selection.id) {
314                            cursor.column = *column
315                        }
316                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
317                        selection.collapse_to(cursor, selection.goal)
318                    });
319                    if vim.state().mode == Mode::VisualBlock {
320                        s.select_anchors(vec![s.first_anchor()])
321                    }
322                });
323            })
324        });
325        vim.switch_mode(Mode::Normal, true, cx);
326    });
327}
328
329pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
330    Vim::update(cx, |vim, cx| {
331        vim.update_active_editor(cx, |editor, cx| {
332            let line_mode = editor.selections.line_mode;
333            copy_selections_content(editor, line_mode, cx);
334            editor.change_selections(None, cx, |s| {
335                s.move_with(|map, selection| {
336                    if line_mode {
337                        selection.start = start_of_line(map, false, selection.start);
338                    };
339                    selection.collapse_to(selection.start, SelectionGoal::None)
340                });
341                if vim.state().mode == Mode::VisualBlock {
342                    s.select_anchors(vec![s.first_anchor()])
343                }
344            });
345        });
346        vim.switch_mode(Mode::Normal, true, cx);
347    });
348}
349
350pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
351    Vim::update(cx, |vim, cx| {
352        vim.stop_recording();
353        vim.update_active_editor(cx, |editor, cx| {
354            editor.transact(cx, |editor, cx| {
355                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
356
357                // Selections are biased right at the start. So we need to store
358                // anchors that are biased left so that we can restore the selections
359                // after the change
360                let stable_anchors = editor
361                    .selections
362                    .disjoint_anchors()
363                    .into_iter()
364                    .map(|selection| {
365                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
366                        start..start
367                    })
368                    .collect::<Vec<_>>();
369
370                let mut edits = Vec::new();
371                for selection in selections.iter() {
372                    let selection = selection.clone();
373                    for row_range in
374                        movement::split_display_range_by_lines(&display_map, selection.range())
375                    {
376                        let range = row_range.start.to_offset(&display_map, Bias::Right)
377                            ..row_range.end.to_offset(&display_map, Bias::Right);
378                        let text = text.repeat(range.len());
379                        edits.push((range, text));
380                    }
381                }
382
383                editor.buffer().update(cx, |buffer, cx| {
384                    buffer.edit(edits, None, cx);
385                });
386                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
387            });
388        });
389        vim.switch_mode(Mode::Normal, false, cx);
390    });
391}
392
393pub fn select_next(
394    _: &mut Workspace,
395    _: &SelectNext,
396    cx: &mut ViewContext<Workspace>,
397) -> Result<()> {
398    Vim::update(cx, |vim, cx| {
399        let count =
400            vim.take_count(cx)
401                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
402        vim.update_active_editor(cx, |editor, cx| {
403            for _ in 0..count {
404                match editor.select_next(&Default::default(), cx) {
405                    Err(a) => return Err(a),
406                    _ => {}
407                }
408            }
409            Ok(())
410        })
411    })
412    .unwrap_or(Ok(()))
413}
414
415pub fn select_previous(
416    _: &mut Workspace,
417    _: &SelectPrevious,
418    cx: &mut ViewContext<Workspace>,
419) -> Result<()> {
420    Vim::update(cx, |vim, cx| {
421        let count =
422            vim.take_count(cx)
423                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
424        vim.update_active_editor(cx, |editor, cx| {
425            for _ in 0..count {
426                match editor.select_previous(&Default::default(), cx) {
427                    Err(a) => return Err(a),
428                    _ => {}
429                }
430            }
431            Ok(())
432        })
433    })
434    .unwrap_or(Ok(()))
435}
436
437#[cfg(test)]
438mod test {
439    use indoc::indoc;
440    use workspace::item::Item;
441
442    use crate::{
443        state::Mode,
444        test::{NeovimBackedTestContext, VimTestContext},
445    };
446
447    #[gpui::test]
448    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
449        let mut cx = NeovimBackedTestContext::new(cx).await;
450
451        cx.set_shared_state(indoc! {
452            "The ˇquick brown
453            fox jumps over
454            the lazy dog"
455        })
456        .await;
457        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
458
459        // entering visual mode should select the character
460        // under cursor
461        cx.simulate_shared_keystrokes(["v"]).await;
462        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
463            fox jumps over
464            the lazy dog"})
465            .await;
466        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
467
468        // forwards motions should extend the selection
469        cx.simulate_shared_keystrokes(["w", "j"]).await;
470        cx.assert_shared_state(indoc! { "The «quick brown
471            fox jumps oˇ»ver
472            the lazy dog"})
473            .await;
474
475        cx.simulate_shared_keystrokes(["escape"]).await;
476        assert_eq!(Mode::Normal, cx.neovim_mode().await);
477        cx.assert_shared_state(indoc! { "The quick brown
478            fox jumps ˇover
479            the lazy dog"})
480            .await;
481
482        // motions work backwards
483        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
484        cx.assert_shared_state(indoc! { "The «ˇquick brown
485            fox jumps o»ver
486            the lazy dog"})
487            .await;
488
489        // works on empty lines
490        cx.set_shared_state(indoc! {"
491            a
492            ˇ
493            b
494            "})
495            .await;
496        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
497        cx.simulate_shared_keystrokes(["v"]).await;
498        cx.assert_shared_state(indoc! {"
499            a
500            «
501            ˇ»b
502        "})
503            .await;
504        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
505
506        // toggles off again
507        cx.simulate_shared_keystrokes(["v"]).await;
508        cx.assert_shared_state(indoc! {"
509            a
510            ˇ
511            b
512            "})
513            .await;
514
515        // works at the end of a document
516        cx.set_shared_state(indoc! {"
517            a
518            b
519            ˇ"})
520            .await;
521
522        cx.simulate_shared_keystrokes(["v"]).await;
523        cx.assert_shared_state(indoc! {"
524            a
525            b
526            ˇ"})
527            .await;
528        assert_eq!(cx.mode(), cx.neovim_mode().await);
529    }
530
531    #[gpui::test]
532    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
533        let mut cx = NeovimBackedTestContext::new(cx).await;
534
535        cx.set_shared_state(indoc! {
536            "The ˇquick brown
537            fox jumps over
538            the lazy dog"
539        })
540        .await;
541        cx.simulate_shared_keystrokes(["shift-v"]).await;
542        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
543            fox jumps over
544            the lazy dog"})
545            .await;
546        assert_eq!(cx.mode(), cx.neovim_mode().await);
547        cx.simulate_shared_keystrokes(["x"]).await;
548        cx.assert_shared_state(indoc! { "fox ˇjumps over
549        the lazy dog"})
550            .await;
551
552        // it should work on empty lines
553        cx.set_shared_state(indoc! {"
554            a
555            ˇ
556            b"})
557            .await;
558        cx.simulate_shared_keystrokes(["shift-v"]).await;
559        cx.assert_shared_state(indoc! { "
560            a
561            «
562            ˇ»b"})
563            .await;
564        cx.simulate_shared_keystrokes(["x"]).await;
565        cx.assert_shared_state(indoc! { "
566            a
567            ˇb"})
568            .await;
569
570        // it should work at the end of the document
571        cx.set_shared_state(indoc! {"
572            a
573            b
574            ˇ"})
575            .await;
576        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
577        cx.simulate_shared_keystrokes(["shift-v"]).await;
578        cx.assert_shared_state(indoc! {"
579            a
580            b
581            ˇ"})
582            .await;
583        assert_eq!(cx.mode(), cx.neovim_mode().await);
584        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
585        cx.simulate_shared_keystrokes(["x"]).await;
586        cx.assert_shared_state(indoc! {"
587            a
588            ˇb"})
589            .await;
590    }
591
592    #[gpui::test]
593    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
594        let mut cx = NeovimBackedTestContext::new(cx).await;
595
596        cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
597            .await;
598
599        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
600            .await;
601        cx.assert_binding_matches(
602            ["v", "w", "j", "x"],
603            indoc! {"
604                The ˇquick brown
605                fox jumps over
606                the lazy dog"},
607        )
608        .await;
609        // Test pasting code copied on delete
610        cx.simulate_shared_keystrokes(["j", "p"]).await;
611        cx.assert_state_matches().await;
612
613        let mut cx = cx.binding(["v", "w", "j", "x"]);
614        cx.assert_all(indoc! {"
615                The ˇquick brown
616                fox jumps over
617                the ˇlazy dog"})
618            .await;
619        let mut cx = cx.binding(["v", "b", "k", "x"]);
620        cx.assert_all(indoc! {"
621                The ˇquick brown
622                fox jumps ˇover
623                the ˇlazy dog"})
624            .await;
625    }
626
627    #[gpui::test]
628    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
629        let mut cx = NeovimBackedTestContext::new(cx).await;
630
631        cx.set_shared_state(indoc! {"
632                The quˇick brown
633                fox jumps over
634                the lazy dog"})
635            .await;
636        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
637        cx.assert_state_matches().await;
638
639        // Test pasting code copied on delete
640        cx.simulate_shared_keystroke("p").await;
641        cx.assert_state_matches().await;
642
643        cx.set_shared_state(indoc! {"
644                The quick brown
645                fox jumps over
646                the laˇzy dog"})
647            .await;
648        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
649        cx.assert_state_matches().await;
650        cx.assert_shared_clipboard("the lazy dog\n").await;
651
652        for marked_text in cx.each_marked_position(indoc! {"
653                        The quˇick brown
654                        fox jumps over
655                        the lazy dog"})
656        {
657            cx.set_shared_state(&marked_text).await;
658            cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
659            cx.assert_state_matches().await;
660            // Test pasting code copied on delete
661            cx.simulate_shared_keystroke("p").await;
662            cx.assert_state_matches().await;
663        }
664
665        cx.set_shared_state(indoc! {"
666            The ˇlong line
667            should not
668            crash
669            "})
670            .await;
671        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
672        cx.assert_state_matches().await;
673    }
674
675    #[gpui::test]
676    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
677        let mut cx = NeovimBackedTestContext::new(cx).await;
678
679        cx.set_shared_state("The quick ˇbrown").await;
680        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
681        cx.assert_shared_state("The quick ˇbrown").await;
682        cx.assert_shared_clipboard("brown").await;
683
684        cx.set_shared_state(indoc! {"
685                The ˇquick brown
686                fox jumps over
687                the lazy dog"})
688            .await;
689        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
690        cx.assert_shared_state(indoc! {"
691                    The ˇquick brown
692                    fox jumps over
693                    the lazy dog"})
694            .await;
695        cx.assert_shared_clipboard(indoc! {"
696                quick brown
697                fox jumps o"})
698            .await;
699
700        cx.set_shared_state(indoc! {"
701                    The quick brown
702                    fox jumps over
703                    the ˇlazy dog"})
704            .await;
705        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
706        cx.assert_shared_state(indoc! {"
707                    The quick brown
708                    fox jumps over
709                    the ˇlazy dog"})
710            .await;
711        cx.assert_shared_clipboard("lazy d").await;
712        cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
713        cx.assert_shared_clipboard("the lazy dog\n").await;
714
715        let mut cx = cx.binding(["v", "b", "k", "y"]);
716        cx.set_shared_state(indoc! {"
717                    The ˇquick brown
718                    fox jumps over
719                    the lazy dog"})
720            .await;
721        cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
722        cx.assert_shared_state(indoc! {"
723                    ˇThe quick brown
724                    fox jumps over
725                    the lazy dog"})
726            .await;
727        cx.assert_clipboard_content(Some("The q"));
728
729        cx.set_shared_state(indoc! {"
730                    The quick brown
731                    fox ˇjumps over
732                    the lazy dog"})
733            .await;
734        cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
735            .await;
736        cx.assert_shared_state(indoc! {"
737                    The quick brown
738                    ˇfox jumps over
739                    the lazy dog"})
740            .await;
741        cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
742            .await;
743    }
744
745    #[gpui::test]
746    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
747        let mut cx = NeovimBackedTestContext::new(cx).await;
748
749        cx.set_shared_state(indoc! {
750            "The ˇquick brown
751             fox jumps over
752             the lazy dog"
753        })
754        .await;
755        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
756        cx.assert_shared_state(indoc! {
757            "The «qˇ»uick brown
758            fox jumps over
759            the lazy dog"
760        })
761        .await;
762        cx.simulate_shared_keystrokes(["2", "down"]).await;
763        cx.assert_shared_state(indoc! {
764            "The «qˇ»uick brown
765            fox «jˇ»umps over
766            the «lˇ»azy dog"
767        })
768        .await;
769        cx.simulate_shared_keystrokes(["e"]).await;
770        cx.assert_shared_state(indoc! {
771            "The «quicˇ»k brown
772            fox «jumpˇ»s over
773            the «lazyˇ» dog"
774        })
775        .await;
776        cx.simulate_shared_keystrokes(["^"]).await;
777        cx.assert_shared_state(indoc! {
778            "«ˇThe q»uick brown
779            «ˇfox j»umps over
780            «ˇthe l»azy dog"
781        })
782        .await;
783        cx.simulate_shared_keystrokes(["$"]).await;
784        cx.assert_shared_state(indoc! {
785            "The «quick brownˇ»
786            fox «jumps overˇ»
787            the «lazy dogˇ»"
788        })
789        .await;
790        cx.simulate_shared_keystrokes(["shift-f", " "]).await;
791        cx.assert_shared_state(indoc! {
792            "The «quickˇ» brown
793            fox «jumpsˇ» over
794            the «lazy ˇ»dog"
795        })
796        .await;
797
798        // toggling through visual mode works as expected
799        cx.simulate_shared_keystrokes(["v"]).await;
800        cx.assert_shared_state(indoc! {
801            "The «quick brown
802            fox jumps over
803            the lazy ˇ»dog"
804        })
805        .await;
806        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
807        cx.assert_shared_state(indoc! {
808            "The «quickˇ» brown
809            fox «jumpsˇ» over
810            the «lazy ˇ»dog"
811        })
812        .await;
813
814        cx.set_shared_state(indoc! {
815            "The ˇquick
816             brown
817             fox
818             jumps over the
819
820             lazy dog
821            "
822        })
823        .await;
824        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
825            .await;
826        cx.assert_shared_state(indoc! {
827            "The«ˇ q»uick
828            bro«ˇwn»
829            foxˇ
830            jumps over the
831
832            lazy dog
833            "
834        })
835        .await;
836        cx.simulate_shared_keystrokes(["down"]).await;
837        cx.assert_shared_state(indoc! {
838            "The «qˇ»uick
839            brow«nˇ»
840            fox
841            jump«sˇ» over the
842
843            lazy dog
844            "
845        })
846        .await;
847        cx.simulate_shared_keystroke("left").await;
848        cx.assert_shared_state(indoc! {
849            "The«ˇ q»uick
850            bro«ˇwn»
851            foxˇ
852            jum«ˇps» over the
853
854            lazy dog
855            "
856        })
857        .await;
858        cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
859        cx.assert_shared_state(indoc! {
860            "Theˇouick
861            broo
862            foxo
863            jumo over the
864
865            lazy dog
866            "
867        })
868        .await;
869
870        //https://github.com/zed-industries/community/issues/1950
871        cx.set_shared_state(indoc! {
872            "Theˇ quick brown
873
874            fox jumps over
875            the lazy dog
876            "
877        })
878        .await;
879        cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
880            .await;
881        cx.assert_shared_state(indoc! {
882            "The «qˇ»uick brown
883
884            fox «jˇ»umps over
885            the lazy dog
886            "
887        })
888        .await;
889    }
890
891    #[gpui::test]
892    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
893        let mut cx = NeovimBackedTestContext::new(cx).await;
894
895        cx.set_shared_state(indoc! {
896            "ˇThe quick brown
897            fox jumps over
898            the lazy dog
899            "
900        })
901        .await;
902        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
903        cx.assert_shared_state(indoc! {
904            "«Tˇ»he quick brown
905            «fˇ»ox jumps over
906            «tˇ»he lazy dog
907            ˇ"
908        })
909        .await;
910
911        cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
912            .await;
913        cx.assert_shared_state(indoc! {
914            "ˇkThe quick brown
915            kfox jumps over
916            kthe lazy dog
917            k"
918        })
919        .await;
920
921        cx.set_shared_state(indoc! {
922            "ˇThe quick brown
923            fox jumps over
924            the lazy dog
925            "
926        })
927        .await;
928        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
929        cx.assert_shared_state(indoc! {
930            "«Tˇ»he quick brown
931            «fˇ»ox jumps over
932            «tˇ»he lazy dog
933            ˇ"
934        })
935        .await;
936        cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
937        cx.assert_shared_state(indoc! {
938            "ˇkhe quick brown
939            kox jumps over
940            khe lazy dog
941            k"
942        })
943        .await;
944    }
945
946    #[gpui::test]
947    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
948        let mut cx = NeovimBackedTestContext::new(cx).await;
949
950        cx.set_shared_state("hello (in [parˇens] o)").await;
951        cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
952        cx.simulate_shared_keystrokes(["a", "]"]).await;
953        cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
954        assert_eq!(cx.mode(), Mode::Visual);
955        cx.simulate_shared_keystrokes(["i", "("]).await;
956        cx.assert_shared_state("hello («in [parens] oˇ»)").await;
957
958        cx.set_shared_state("hello in a wˇord again.").await;
959        cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
960            .await;
961        cx.assert_shared_state("hello in a w«ordˇ» again.").await;
962        assert_eq!(cx.mode(), Mode::VisualBlock);
963        cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
964        cx.assert_shared_state("«ˇhello in a word» again.").await;
965        assert_eq!(cx.mode(), Mode::Visual);
966    }
967
968    #[gpui::test]
969    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
970        let mut cx = VimTestContext::new(cx, true).await;
971
972        cx.set_state("aˇbc", Mode::Normal);
973        cx.simulate_keystrokes(["ctrl-v"]);
974        assert_eq!(cx.mode(), Mode::VisualBlock);
975        cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
976        assert_eq!(cx.mode(), Mode::VisualBlock);
977    }
978}