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.record_current_action(cx);
281        vim.update_active_editor(cx, |editor, cx| {
282            let mut original_columns: HashMap<_, _> = Default::default();
283            let line_mode = editor.selections.line_mode;
284
285            editor.transact(cx, |editor, cx| {
286                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
287                    s.move_with(|map, selection| {
288                        if line_mode {
289                            let mut position = selection.head();
290                            if !selection.reversed {
291                                position = movement::left(map, position);
292                            }
293                            original_columns.insert(selection.id, position.to_point(map).column);
294                        }
295                        selection.goal = SelectionGoal::None;
296                    });
297                });
298                copy_selections_content(editor, line_mode, cx);
299                editor.insert("", cx);
300
301                // Fixup cursor position after the deletion
302                editor.set_clip_at_line_ends(true, cx);
303                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
304                    s.move_with(|map, selection| {
305                        let mut cursor = selection.head().to_point(map);
306
307                        if let Some(column) = original_columns.get(&selection.id) {
308                            cursor.column = *column
309                        }
310                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
311                        selection.collapse_to(cursor, selection.goal)
312                    });
313                    if vim.state().mode == Mode::VisualBlock {
314                        s.select_anchors(vec![s.first_anchor()])
315                    }
316                });
317            })
318        });
319        vim.switch_mode(Mode::Normal, true, cx);
320    });
321}
322
323pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
324    Vim::update(cx, |vim, cx| {
325        vim.update_active_editor(cx, |editor, cx| {
326            let line_mode = editor.selections.line_mode;
327            copy_selections_content(editor, line_mode, cx);
328            editor.change_selections(None, cx, |s| {
329                s.move_with(|_, selection| {
330                    selection.collapse_to(selection.start, SelectionGoal::None)
331                });
332                if vim.state().mode == Mode::VisualBlock {
333                    s.select_anchors(vec![s.first_anchor()])
334                }
335            });
336        });
337        vim.switch_mode(Mode::Normal, true, cx);
338    });
339}
340
341pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
342    Vim::update(cx, |vim, cx| {
343        vim.stop_recording();
344        vim.update_active_editor(cx, |editor, cx| {
345            editor.transact(cx, |editor, cx| {
346                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
347
348                // Selections are biased right at the start. So we need to store
349                // anchors that are biased left so that we can restore the selections
350                // after the change
351                let stable_anchors = editor
352                    .selections
353                    .disjoint_anchors()
354                    .into_iter()
355                    .map(|selection| {
356                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
357                        start..start
358                    })
359                    .collect::<Vec<_>>();
360
361                let mut edits = Vec::new();
362                for selection in selections.iter() {
363                    let selection = selection.clone();
364                    for row_range in
365                        movement::split_display_range_by_lines(&display_map, selection.range())
366                    {
367                        let range = row_range.start.to_offset(&display_map, Bias::Right)
368                            ..row_range.end.to_offset(&display_map, Bias::Right);
369                        let text = text.repeat(range.len());
370                        edits.push((range, text));
371                    }
372                }
373
374                editor.buffer().update(cx, |buffer, cx| {
375                    buffer.edit(edits, None, cx);
376                });
377                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
378            });
379        });
380        vim.switch_mode(Mode::Normal, false, cx);
381    });
382}
383
384#[cfg(test)]
385mod test {
386    use indoc::indoc;
387    use workspace::item::Item;
388
389    use crate::{
390        state::Mode,
391        test::{NeovimBackedTestContext, VimTestContext},
392    };
393
394    #[gpui::test]
395    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
396        let mut cx = NeovimBackedTestContext::new(cx).await;
397
398        cx.set_shared_state(indoc! {
399            "The ˇquick brown
400            fox jumps over
401            the lazy dog"
402        })
403        .await;
404        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
405
406        // entering visual mode should select the character
407        // under cursor
408        cx.simulate_shared_keystrokes(["v"]).await;
409        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
410            fox jumps over
411            the lazy dog"})
412            .await;
413        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
414
415        // forwards motions should extend the selection
416        cx.simulate_shared_keystrokes(["w", "j"]).await;
417        cx.assert_shared_state(indoc! { "The «quick brown
418            fox jumps oˇ»ver
419            the lazy dog"})
420            .await;
421
422        cx.simulate_shared_keystrokes(["escape"]).await;
423        assert_eq!(Mode::Normal, cx.neovim_mode().await);
424        cx.assert_shared_state(indoc! { "The quick brown
425            fox jumps ˇover
426            the lazy dog"})
427            .await;
428
429        // motions work backwards
430        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
431        cx.assert_shared_state(indoc! { "The «ˇquick brown
432            fox jumps o»ver
433            the lazy dog"})
434            .await;
435
436        // works on empty lines
437        cx.set_shared_state(indoc! {"
438            a
439            ˇ
440            b
441            "})
442            .await;
443        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
444        cx.simulate_shared_keystrokes(["v"]).await;
445        cx.assert_shared_state(indoc! {"
446            a
447            «
448            ˇ»b
449        "})
450            .await;
451        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
452
453        // toggles off again
454        cx.simulate_shared_keystrokes(["v"]).await;
455        cx.assert_shared_state(indoc! {"
456            a
457            ˇ
458            b
459            "})
460            .await;
461
462        // works at the end of a document
463        cx.set_shared_state(indoc! {"
464            a
465            b
466            ˇ"})
467            .await;
468
469        cx.simulate_shared_keystrokes(["v"]).await;
470        cx.assert_shared_state(indoc! {"
471            a
472            b
473            ˇ"})
474            .await;
475        assert_eq!(cx.mode(), cx.neovim_mode().await);
476    }
477
478    #[gpui::test]
479    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
480        let mut cx = NeovimBackedTestContext::new(cx).await;
481
482        cx.set_shared_state(indoc! {
483            "The ˇquick brown
484            fox jumps over
485            the lazy dog"
486        })
487        .await;
488        cx.simulate_shared_keystrokes(["shift-v"]).await;
489        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
490            fox jumps over
491            the lazy dog"})
492            .await;
493        assert_eq!(cx.mode(), cx.neovim_mode().await);
494        cx.simulate_shared_keystrokes(["x"]).await;
495        cx.assert_shared_state(indoc! { "fox ˇjumps over
496        the lazy dog"})
497            .await;
498
499        // it should work on empty lines
500        cx.set_shared_state(indoc! {"
501            a
502            ˇ
503            b"})
504            .await;
505        cx.simulate_shared_keystrokes(["shift-v"]).await;
506        cx.assert_shared_state(indoc! { "
507            a
508            «
509            ˇ»b"})
510            .await;
511        cx.simulate_shared_keystrokes(["x"]).await;
512        cx.assert_shared_state(indoc! { "
513            a
514            ˇb"})
515            .await;
516
517        // it should work at the end of the document
518        cx.set_shared_state(indoc! {"
519            a
520            b
521            ˇ"})
522            .await;
523        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
524        cx.simulate_shared_keystrokes(["shift-v"]).await;
525        cx.assert_shared_state(indoc! {"
526            a
527            b
528            ˇ"})
529            .await;
530        assert_eq!(cx.mode(), cx.neovim_mode().await);
531        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
532        cx.simulate_shared_keystrokes(["x"]).await;
533        cx.assert_shared_state(indoc! {"
534            a
535            ˇb"})
536            .await;
537    }
538
539    #[gpui::test]
540    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
541        let mut cx = NeovimBackedTestContext::new(cx).await;
542
543        cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
544            .await;
545
546        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
547            .await;
548        cx.assert_binding_matches(
549            ["v", "w", "j", "x"],
550            indoc! {"
551                The ˇquick brown
552                fox jumps over
553                the lazy dog"},
554        )
555        .await;
556        // Test pasting code copied on delete
557        cx.simulate_shared_keystrokes(["j", "p"]).await;
558        cx.assert_state_matches().await;
559
560        let mut cx = cx.binding(["v", "w", "j", "x"]);
561        cx.assert_all(indoc! {"
562                The ˇquick brown
563                fox jumps over
564                the ˇlazy dog"})
565            .await;
566        let mut cx = cx.binding(["v", "b", "k", "x"]);
567        cx.assert_all(indoc! {"
568                The ˇquick brown
569                fox jumps ˇover
570                the ˇlazy dog"})
571            .await;
572    }
573
574    #[gpui::test]
575    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
576        let mut cx = NeovimBackedTestContext::new(cx).await;
577
578        cx.set_shared_state(indoc! {"
579                The quˇick brown
580                fox jumps over
581                the lazy dog"})
582            .await;
583        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
584        cx.assert_state_matches().await;
585
586        // Test pasting code copied on delete
587        cx.simulate_shared_keystroke("p").await;
588        cx.assert_state_matches().await;
589
590        cx.set_shared_state(indoc! {"
591                The quick brown
592                fox jumps over
593                the laˇzy dog"})
594            .await;
595        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
596        cx.assert_state_matches().await;
597        cx.assert_shared_clipboard("the lazy dog\n").await;
598
599        for marked_text in cx.each_marked_position(indoc! {"
600                        The quˇick brown
601                        fox jumps over
602                        the lazy dog"})
603        {
604            cx.set_shared_state(&marked_text).await;
605            cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
606            cx.assert_state_matches().await;
607            // Test pasting code copied on delete
608            cx.simulate_shared_keystroke("p").await;
609            cx.assert_state_matches().await;
610        }
611
612        cx.set_shared_state(indoc! {"
613            The ˇlong line
614            should not
615            crash
616            "})
617            .await;
618        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
619        cx.assert_state_matches().await;
620    }
621
622    #[gpui::test]
623    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
624        let mut cx = NeovimBackedTestContext::new(cx).await;
625
626        cx.set_shared_state("The quick ˇbrown").await;
627        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
628        cx.assert_shared_state("The quick ˇbrown").await;
629        cx.assert_shared_clipboard("brown").await;
630
631        cx.set_shared_state(indoc! {"
632                The ˇquick brown
633                fox jumps over
634                the lazy dog"})
635            .await;
636        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
637        cx.assert_shared_state(indoc! {"
638                    The ˇquick brown
639                    fox jumps over
640                    the lazy dog"})
641            .await;
642        cx.assert_shared_clipboard(indoc! {"
643                quick brown
644                fox jumps o"})
645            .await;
646
647        cx.set_shared_state(indoc! {"
648                    The quick brown
649                    fox jumps over
650                    the ˇlazy dog"})
651            .await;
652        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
653        cx.assert_shared_state(indoc! {"
654                    The quick brown
655                    fox jumps over
656                    the ˇlazy dog"})
657            .await;
658        cx.assert_shared_clipboard("lazy d").await;
659        cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
660        cx.assert_shared_clipboard("the lazy dog\n").await;
661
662        let mut cx = cx.binding(["v", "b", "k", "y"]);
663        cx.set_shared_state(indoc! {"
664                    The ˇquick brown
665                    fox jumps over
666                    the lazy dog"})
667            .await;
668        cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
669        cx.assert_shared_state(indoc! {"
670                    ˇThe quick brown
671                    fox jumps over
672                    the lazy dog"})
673            .await;
674        cx.assert_clipboard_content(Some("The q"));
675    }
676
677    #[gpui::test]
678    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
679        let mut cx = NeovimBackedTestContext::new(cx).await;
680
681        cx.set_shared_state(indoc! {
682            "The ˇquick brown
683             fox jumps over
684             the lazy dog"
685        })
686        .await;
687        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
688        cx.assert_shared_state(indoc! {
689            "The «qˇ»uick brown
690            fox jumps over
691            the lazy dog"
692        })
693        .await;
694        cx.simulate_shared_keystrokes(["2", "down"]).await;
695        cx.assert_shared_state(indoc! {
696            "The «qˇ»uick brown
697            fox «jˇ»umps over
698            the «lˇ»azy dog"
699        })
700        .await;
701        cx.simulate_shared_keystrokes(["e"]).await;
702        cx.assert_shared_state(indoc! {
703            "The «quicˇ»k brown
704            fox «jumpˇ»s over
705            the «lazyˇ» dog"
706        })
707        .await;
708        cx.simulate_shared_keystrokes(["^"]).await;
709        cx.assert_shared_state(indoc! {
710            "«ˇThe q»uick brown
711            «ˇfox j»umps over
712            «ˇthe l»azy dog"
713        })
714        .await;
715        cx.simulate_shared_keystrokes(["$"]).await;
716        cx.assert_shared_state(indoc! {
717            "The «quick brownˇ»
718            fox «jumps overˇ»
719            the «lazy dogˇ»"
720        })
721        .await;
722        cx.simulate_shared_keystrokes(["shift-f", " "]).await;
723        cx.assert_shared_state(indoc! {
724            "The «quickˇ» brown
725            fox «jumpsˇ» over
726            the «lazy ˇ»dog"
727        })
728        .await;
729
730        // toggling through visual mode works as expected
731        cx.simulate_shared_keystrokes(["v"]).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(["ctrl-v"]).await;
739        cx.assert_shared_state(indoc! {
740            "The «quickˇ» brown
741            fox «jumpsˇ» over
742            the «lazy ˇ»dog"
743        })
744        .await;
745
746        cx.set_shared_state(indoc! {
747            "The ˇquick
748             brown
749             fox
750             jumps over the
751
752             lazy dog
753            "
754        })
755        .await;
756        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
757            .await;
758        cx.assert_shared_state(indoc! {
759            "The«ˇ q»uick
760            bro«ˇwn»
761            foxˇ
762            jumps over the
763
764            lazy dog
765            "
766        })
767        .await;
768        cx.simulate_shared_keystrokes(["down"]).await;
769        cx.assert_shared_state(indoc! {
770            "The «qˇ»uick
771            brow«nˇ»
772            fox
773            jump«sˇ» over the
774
775            lazy dog
776            "
777        })
778        .await;
779        cx.simulate_shared_keystroke("left").await;
780        cx.assert_shared_state(indoc! {
781            "The«ˇ q»uick
782            bro«ˇwn»
783            foxˇ
784            jum«ˇps» over the
785
786            lazy dog
787            "
788        })
789        .await;
790        cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
791        cx.assert_shared_state(indoc! {
792            "Theˇouick
793            broo
794            foxo
795            jumo over the
796
797            lazy dog
798            "
799        })
800        .await;
801
802        //https://github.com/zed-industries/community/issues/1950
803        cx.set_shared_state(indoc! {
804            "Theˇ quick brown
805
806            fox jumps over
807            the lazy dog
808            "
809        })
810        .await;
811        cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
812            .await;
813        cx.assert_shared_state(indoc! {
814            "The «qˇ»uick brown
815
816            fox «jˇ»umps over
817            the lazy dog
818            "
819        })
820        .await;
821    }
822
823    #[gpui::test]
824    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
825        let mut cx = NeovimBackedTestContext::new(cx).await;
826
827        cx.set_shared_state(indoc! {
828            "ˇThe quick brown
829            fox jumps over
830            the lazy dog
831            "
832        })
833        .await;
834        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
835        cx.assert_shared_state(indoc! {
836            "«Tˇ»he quick brown
837            «fˇ»ox jumps over
838            «tˇ»he lazy dog
839            ˇ"
840        })
841        .await;
842
843        cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
844            .await;
845        cx.assert_shared_state(indoc! {
846            "ˇkThe quick brown
847            kfox jumps over
848            kthe lazy dog
849            k"
850        })
851        .await;
852
853        cx.set_shared_state(indoc! {
854            "ˇThe quick brown
855            fox jumps over
856            the lazy dog
857            "
858        })
859        .await;
860        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
861        cx.assert_shared_state(indoc! {
862            "«Tˇ»he quick brown
863            «fˇ»ox jumps over
864            «tˇ»he lazy dog
865            ˇ"
866        })
867        .await;
868        cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
869        cx.assert_shared_state(indoc! {
870            "ˇkhe quick brown
871            kox jumps over
872            khe lazy dog
873            k"
874        })
875        .await;
876    }
877
878    #[gpui::test]
879    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
880        let mut cx = NeovimBackedTestContext::new(cx).await;
881
882        cx.set_shared_state("hello (in [parˇens] o)").await;
883        cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
884        cx.simulate_shared_keystrokes(["a", "]"]).await;
885        cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
886        assert_eq!(cx.mode(), Mode::Visual);
887        cx.simulate_shared_keystrokes(["i", "("]).await;
888        cx.assert_shared_state("hello («in [parens] oˇ»)").await;
889
890        cx.set_shared_state("hello in a wˇord again.").await;
891        cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
892            .await;
893        cx.assert_shared_state("hello in a w«ordˇ» again.").await;
894        assert_eq!(cx.mode(), Mode::VisualBlock);
895        cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
896        cx.assert_shared_state("«ˇhello in a word» again.").await;
897        assert_eq!(cx.mode(), Mode::Visual);
898    }
899
900    #[gpui::test]
901    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
902        let mut cx = VimTestContext::new(cx, true).await;
903
904        cx.set_state("aˇbc", Mode::Normal);
905        cx.simulate_keystrokes(["ctrl-v"]);
906        assert_eq!(cx.mode(), Mode::VisualBlock);
907        cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
908        assert_eq!(cx.mode(), Mode::VisualBlock);
909    }
910}