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