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