visual.rs

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