visual.rs

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