visual.rs

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