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