visual.rs

  1use std::{borrow::Cow, sync::Arc};
  2
  3use collections::HashMap;
  4use editor::{
  5    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
  6};
  7use gpui::{actions, AppContext, ViewContext, WindowContext};
  8use language::{AutoindentMode, SelectionGoal};
  9use workspace::Workspace;
 10
 11use crate::{
 12    motion::Motion,
 13    object::Object,
 14    state::{Mode, Operator},
 15    utils::copy_selections_content,
 16    Vim,
 17};
 18
 19actions!(
 20    vim,
 21    [
 22        ToggleVisual,
 23        ToggleVisualLine,
 24        VisualDelete,
 25        VisualChange,
 26        VisualYank,
 27        VisualPaste,
 28        OtherEnd,
 29    ]
 30);
 31
 32pub fn init(cx: &mut AppContext) {
 33    cx.add_action(toggle_visual);
 34    cx.add_action(toggle_visual_line);
 35    cx.add_action(other_end);
 36    cx.add_action(change);
 37    cx.add_action(delete);
 38    cx.add_action(yank);
 39    cx.add_action(paste);
 40}
 41
 42pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
 43    Vim::update(cx, |vim, cx| {
 44        vim.update_active_editor(cx, |editor, cx| {
 45            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 46                s.move_with(|map, selection| {
 47                    let was_reversed = selection.reversed;
 48
 49                    let mut current_head = selection.head();
 50
 51                    // our motions assume the current character is after the cursor,
 52                    // but in (forward) visual mode the current character is just
 53                    // before the end of the selection.
 54                    if !selection.reversed {
 55                        current_head = movement::left(map, selection.end)
 56                    }
 57
 58                    let Some((new_head, goal)) =
 59                        motion.move_point(map, current_head, selection.goal, times) else { return };
 60
 61                    selection.set_head(new_head, goal);
 62
 63                    // ensure the current character is included in the selection.
 64                    if !selection.reversed {
 65                        selection.end = movement::right(map, selection.end)
 66                    }
 67
 68                    // vim always ensures the anchor character stays selected.
 69                    // if our selection has reversed, we need to move the opposite end
 70                    // to ensure the anchor is still selected.
 71                    if was_reversed && !selection.reversed {
 72                        selection.start = movement::left(map, selection.start);
 73                    } else if !was_reversed && selection.reversed {
 74                        selection.end = movement::right(map, selection.end);
 75                    }
 76                });
 77            });
 78        });
 79    });
 80}
 81
 82pub fn visual_object(object: Object, cx: &mut WindowContext) {
 83    Vim::update(cx, |vim, cx| {
 84        if let Some(Operator::Object { around }) = vim.active_operator() {
 85            vim.pop_operator(cx);
 86
 87            vim.update_active_editor(cx, |editor, cx| {
 88                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 89                    s.move_with(|map, selection| {
 90                        let mut head = selection.head();
 91
 92                        // all our motions assume that the current character is
 93                        // after the cursor; however in the case of a visual selection
 94                        // the current character is before the cursor.
 95                        if !selection.reversed {
 96                            head = movement::left(map, head);
 97                        }
 98
 99                        if let Some(range) = object.range(map, head, around) {
100                            if !range.is_empty() {
101                                let expand_both_ways = if selection.is_empty() {
102                                    true
103                                // contains only one character
104                                } else if let Some((_, start)) =
105                                    map.reverse_chars_at(selection.end).next()
106                                {
107                                    selection.start == start
108                                } else {
109                                    false
110                                };
111
112                                if expand_both_ways {
113                                    selection.start = range.start;
114                                    selection.end = range.end;
115                                } else if selection.reversed {
116                                    selection.start = range.start;
117                                } else {
118                                    selection.end = range.end;
119                                }
120                            }
121                        }
122                    });
123                });
124            });
125        }
126    });
127}
128
129pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext<Workspace>) {
130    Vim::update(cx, |vim, cx| match vim.state.mode {
131        Mode::Normal | Mode::Insert | Mode::Visual { line: true } => {
132            vim.switch_mode(Mode::Visual { line: false }, false, cx);
133        }
134        Mode::Visual { line: false } => {
135            vim.switch_mode(Mode::Normal, false, cx);
136        }
137    })
138}
139
140pub fn toggle_visual_line(
141    _: &mut Workspace,
142    _: &ToggleVisualLine,
143    cx: &mut ViewContext<Workspace>,
144) {
145    Vim::update(cx, |vim, cx| match vim.state.mode {
146        Mode::Normal | Mode::Insert | Mode::Visual { line: false } => {
147            vim.switch_mode(Mode::Visual { line: true }, false, cx);
148        }
149        Mode::Visual { line: true } => {
150            vim.switch_mode(Mode::Normal, false, cx);
151        }
152    })
153}
154
155pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
156    Vim::update(cx, |vim, cx| {
157        vim.update_active_editor(cx, |editor, cx| {
158            editor.change_selections(None, cx, |s| {
159                s.move_with(|_, selection| {
160                    selection.reversed = !selection.reversed;
161                })
162            })
163        })
164    });
165}
166
167pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
168    Vim::update(cx, |vim, cx| {
169        vim.update_active_editor(cx, |editor, cx| {
170            // Compute edits and resulting anchor selections. If in line mode, adjust
171            // the anchor location and additional newline
172            let mut edits = Vec::new();
173            let mut new_selections = Vec::new();
174            let line_mode = editor.selections.line_mode;
175            editor.change_selections(None, cx, |s| {
176                s.move_with(|map, selection| {
177                    if line_mode {
178                        let range = selection.map(|p| p.to_point(map)).range();
179                        let expanded_range = map.expand_to_line(range);
180                        // If we are at the last line, the anchor needs to be after the newline so that
181                        // it is on a line of its own. Otherwise, the anchor may be after the newline
182                        let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
183                            map.buffer_snapshot.anchor_after(expanded_range.end)
184                        } else {
185                            map.buffer_snapshot.anchor_before(expanded_range.start)
186                        };
187
188                        edits.push((expanded_range, "\n"));
189                        new_selections.push(selection.map(|_| anchor));
190                    } else {
191                        let range = selection.map(|p| p.to_point(map)).range();
192                        let anchor = map.buffer_snapshot.anchor_after(range.end);
193                        edits.push((range, ""));
194                        new_selections.push(selection.map(|_| anchor));
195                    }
196                    selection.goal = SelectionGoal::None;
197                });
198            });
199            copy_selections_content(editor, editor.selections.line_mode, cx);
200            editor.edit_with_autoindent(edits, cx);
201            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
202                s.select_anchors(new_selections);
203            });
204        });
205        vim.switch_mode(Mode::Insert, true, cx);
206    });
207}
208
209pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
210    Vim::update(cx, |vim, cx| {
211        vim.update_active_editor(cx, |editor, cx| {
212            let mut original_columns: HashMap<_, _> = Default::default();
213            let line_mode = editor.selections.line_mode;
214
215            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
216                s.move_with(|map, selection| {
217                    if line_mode {
218                        let mut position = selection.head();
219                        if !selection.reversed {
220                            position = movement::left(map, position);
221                        }
222                        original_columns.insert(selection.id, position.to_point(map).column);
223                    }
224                    selection.goal = SelectionGoal::None;
225                });
226            });
227            copy_selections_content(editor, line_mode, cx);
228            editor.insert("", cx);
229
230            // Fixup cursor position after the deletion
231            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232                s.move_with(|map, selection| {
233                    let mut cursor = selection.head().to_point(map);
234
235                    if let Some(column) = original_columns.get(&selection.id) {
236                        if *column < map.line_len(cursor.row) {
237                            cursor.column = *column;
238                        } else {
239                            cursor.column = map.line_len(cursor.row).saturating_sub(1);
240                        }
241                    }
242                    let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
243                    selection.collapse_to(cursor, selection.goal)
244                });
245            });
246        });
247        vim.switch_mode(Mode::Normal, true, cx);
248    });
249}
250
251pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
252    Vim::update(cx, |vim, cx| {
253        vim.update_active_editor(cx, |editor, cx| {
254            let line_mode = editor.selections.line_mode;
255            copy_selections_content(editor, line_mode, cx);
256            editor.change_selections(None, cx, |s| {
257                s.move_with(|_, selection| {
258                    selection.collapse_to(selection.start, SelectionGoal::None)
259                });
260            });
261        });
262        vim.switch_mode(Mode::Normal, true, cx);
263    });
264}
265
266pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
267    Vim::update(cx, |vim, cx| {
268        vim.update_active_editor(cx, |editor, cx| {
269            editor.transact(cx, |editor, cx| {
270                if let Some(item) = cx.read_from_clipboard() {
271                    copy_selections_content(editor, editor.selections.line_mode, cx);
272                    let mut clipboard_text = Cow::Borrowed(item.text());
273                    if let Some(mut clipboard_selections) =
274                        item.metadata::<Vec<ClipboardSelection>>()
275                    {
276                        let (display_map, selections) = editor.selections.all_adjusted_display(cx);
277                        let all_selections_were_entire_line =
278                            clipboard_selections.iter().all(|s| s.is_entire_line);
279                        if clipboard_selections.len() != selections.len() {
280                            let mut newline_separated_text = String::new();
281                            let mut clipboard_selections =
282                                clipboard_selections.drain(..).peekable();
283                            let mut ix = 0;
284                            while let Some(clipboard_selection) = clipboard_selections.next() {
285                                newline_separated_text
286                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
287                                ix += clipboard_selection.len;
288                                if clipboard_selections.peek().is_some() {
289                                    newline_separated_text.push('\n');
290                                }
291                            }
292                            clipboard_text = Cow::Owned(newline_separated_text);
293                        }
294
295                        let mut new_selections = Vec::new();
296                        editor.buffer().update(cx, |buffer, cx| {
297                            let snapshot = buffer.snapshot(cx);
298                            let mut start_offset = 0;
299                            let mut edits = Vec::new();
300                            for (ix, selection) in selections.iter().enumerate() {
301                                let to_insert;
302                                let linewise;
303                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
304                                    let end_offset = start_offset + clipboard_selection.len;
305                                    to_insert = &clipboard_text[start_offset..end_offset];
306                                    linewise = clipboard_selection.is_entire_line;
307                                    start_offset = end_offset;
308                                } else {
309                                    to_insert = clipboard_text.as_str();
310                                    linewise = all_selections_were_entire_line;
311                                }
312
313                                let mut selection = selection.clone();
314                                if !selection.reversed {
315                                    let adjusted = selection.end;
316                                    // If the selection is empty, move both the start and end forward one
317                                    // character
318                                    if selection.is_empty() {
319                                        selection.start = adjusted;
320                                        selection.end = adjusted;
321                                    } else {
322                                        selection.end = adjusted;
323                                    }
324                                }
325
326                                let range = selection.map(|p| p.to_point(&display_map)).range();
327
328                                let new_position = if linewise {
329                                    edits.push((range.start..range.start, "\n"));
330                                    let mut new_position = range.start;
331                                    new_position.column = 0;
332                                    new_position.row += 1;
333                                    new_position
334                                } else {
335                                    range.start
336                                };
337
338                                new_selections.push(selection.map(|_| new_position));
339
340                                if linewise && to_insert.ends_with('\n') {
341                                    edits.push((
342                                        range.clone(),
343                                        &to_insert[0..to_insert.len().saturating_sub(1)],
344                                    ))
345                                } else {
346                                    edits.push((range.clone(), to_insert));
347                                }
348
349                                if linewise {
350                                    edits.push((range.end..range.end, "\n"));
351                                }
352                            }
353                            drop(snapshot);
354                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
355                        });
356
357                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
358                            s.select(new_selections)
359                        });
360                    } else {
361                        editor.insert(&clipboard_text, cx);
362                    }
363                }
364            });
365        });
366        vim.switch_mode(Mode::Normal, true, cx);
367    });
368}
369
370pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
371    Vim::update(cx, |vim, cx| {
372        vim.update_active_editor(cx, |editor, cx| {
373            editor.transact(cx, |editor, cx| {
374                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
375
376                // Selections are biased right at the start. So we need to store
377                // anchors that are biased left so that we can restore the selections
378                // after the change
379                let stable_anchors = editor
380                    .selections
381                    .disjoint_anchors()
382                    .into_iter()
383                    .map(|selection| {
384                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
385                        start..start
386                    })
387                    .collect::<Vec<_>>();
388
389                let mut edits = Vec::new();
390                for selection in selections.iter() {
391                    let selection = selection.clone();
392                    for row_range in
393                        movement::split_display_range_by_lines(&display_map, selection.range())
394                    {
395                        let range = row_range.start.to_offset(&display_map, Bias::Right)
396                            ..row_range.end.to_offset(&display_map, Bias::Right);
397                        let text = text.repeat(range.len());
398                        edits.push((range, text));
399                    }
400                }
401
402                editor.buffer().update(cx, |buffer, cx| {
403                    buffer.edit(edits, None, cx);
404                });
405                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
406            });
407        });
408        vim.switch_mode(Mode::Normal, false, cx);
409    });
410}
411
412#[cfg(test)]
413mod test {
414    use indoc::indoc;
415    use workspace::item::Item;
416
417    use crate::{
418        state::Mode,
419        test::{NeovimBackedTestContext, VimTestContext},
420    };
421
422    #[gpui::test]
423    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
424        let mut cx = NeovimBackedTestContext::new(cx).await;
425
426        cx.set_shared_state(indoc! {
427            "The ˇquick brown
428            fox jumps over
429            the lazy dog"
430        })
431        .await;
432        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
433
434        // entering visual mode should select the character
435        // under cursor
436        cx.simulate_shared_keystrokes(["v"]).await;
437        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
438            fox jumps over
439            the lazy dog"})
440            .await;
441        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
442
443        // forwards motions should extend the selection
444        cx.simulate_shared_keystrokes(["w", "j"]).await;
445        cx.assert_shared_state(indoc! { "The «quick brown
446            fox jumps oˇ»ver
447            the lazy dog"})
448            .await;
449
450        cx.simulate_shared_keystrokes(["escape"]).await;
451        assert_eq!(Mode::Normal, cx.neovim_mode().await);
452        cx.assert_shared_state(indoc! { "The quick brown
453            fox jumps ˇover
454            the lazy dog"})
455            .await;
456
457        // motions work backwards
458        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
459        cx.assert_shared_state(indoc! { "The «ˇquick brown
460            fox jumps o»ver
461            the lazy dog"})
462            .await;
463
464        // works on empty lines
465        cx.set_shared_state(indoc! {"
466            a
467            ˇ
468            b
469            "})
470            .await;
471        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
472        cx.simulate_shared_keystrokes(["v"]).await;
473        cx.assert_shared_state(indoc! {"
474            a
475            «
476            ˇ»b
477        "})
478            .await;
479        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
480
481        // toggles off again
482        cx.simulate_shared_keystrokes(["v"]).await;
483        cx.assert_shared_state(indoc! {"
484            a
485            ˇ
486            b
487            "})
488            .await;
489
490        // works at the end of a document
491        cx.set_shared_state(indoc! {"
492            a
493            b
494            ˇ"})
495            .await;
496
497        cx.simulate_shared_keystrokes(["v"]).await;
498        cx.assert_shared_state(indoc! {"
499            a
500            b
501            ˇ"})
502            .await;
503        assert_eq!(cx.mode(), cx.neovim_mode().await);
504    }
505
506    #[gpui::test]
507    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
508        let mut cx = NeovimBackedTestContext::new(cx).await;
509
510        cx.set_shared_state(indoc! {
511            "The ˇquick brown
512            fox jumps over
513            the lazy dog"
514        })
515        .await;
516        cx.simulate_shared_keystrokes(["shift-v"]).await;
517        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
518            fox jumps over
519            the lazy dog"})
520            .await;
521        assert_eq!(cx.mode(), cx.neovim_mode().await);
522        cx.simulate_shared_keystrokes(["x"]).await;
523        cx.assert_shared_state(indoc! { "fox ˇjumps over
524        the lazy dog"})
525            .await;
526
527        // it should work on empty lines
528        cx.set_shared_state(indoc! {"
529            a
530            ˇ
531            b"})
532            .await;
533        cx.simulate_shared_keystrokes(["shift-v"]).await;
534        cx.assert_shared_state(indoc! { "
535            a
536            «
537            ˇ»b"})
538            .await;
539        cx.simulate_shared_keystrokes(["x"]).await;
540        cx.assert_shared_state(indoc! { "
541            a
542            ˇb"})
543            .await;
544    }
545
546    #[gpui::test]
547    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
548        let mut cx = NeovimBackedTestContext::new(cx).await;
549
550        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
551            .await;
552        cx.assert_binding_matches(
553            ["v", "w", "j", "x"],
554            indoc! {"
555                The ˇquick brown
556                fox jumps over
557                the lazy dog"},
558        )
559        .await;
560        // Test pasting code copied on delete
561        cx.simulate_shared_keystrokes(["j", "p"]).await;
562        cx.assert_state_matches().await;
563
564        let mut cx = cx.binding(["v", "w", "j", "x"]);
565        cx.assert_all(indoc! {"
566                The ˇquick brown
567                fox jumps over
568                the ˇlazy dog"})
569            .await;
570        let mut cx = cx.binding(["v", "b", "k", "x"]);
571        cx.assert_all(indoc! {"
572                The ˇquick brown
573                fox jumps ˇover
574                the ˇlazy dog"})
575            .await;
576    }
577
578    #[gpui::test]
579    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
580        let mut cx = NeovimBackedTestContext::new(cx)
581            .await
582            .binding(["shift-v", "x"]);
583        cx.assert(indoc! {"
584                The quˇick brown
585                fox jumps over
586                the lazy dog"})
587            .await;
588        // Test pasting code copied on delete
589        cx.simulate_shared_keystroke("p").await;
590        cx.assert_state_matches().await;
591
592        cx.assert_all(indoc! {"
593                The quick brown
594                fox juˇmps over
595                the laˇzy dog"})
596            .await;
597        let mut cx = cx.binding(["shift-v", "j", "x"]);
598        cx.assert(indoc! {"
599                The quˇick brown
600                fox jumps over
601                the lazy dog"})
602            .await;
603        // Test pasting code copied on delete
604        cx.simulate_shared_keystroke("p").await;
605        cx.assert_state_matches().await;
606
607        cx.assert_all(indoc! {"
608                The quick brown
609                fox juˇmps over
610                the laˇzy dog"})
611            .await;
612
613        cx.set_shared_state(indoc! {"
614            The ˇlong line
615            should not
616            crash
617            "})
618            .await;
619        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
620        cx.assert_state_matches().await;
621    }
622
623    #[gpui::test]
624    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
625        let mut cx = NeovimBackedTestContext::new(cx).await;
626
627        cx.set_shared_state("The quick ˇbrown").await;
628        cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
629        cx.assert_shared_state("The quick ˇ").await;
630
631        cx.set_shared_state(indoc! {"
632            The ˇquick brown
633            fox jumps over
634            the lazy dog"})
635            .await;
636        cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
637        cx.assert_shared_state(indoc! {"
638            The ˇver
639            the lazy dog"})
640            .await;
641
642        let cases = cx.each_marked_position(indoc! {"
643                         The ˇquick brown
644                         fox jumps ˇover
645                         the ˇlazy dog"});
646        for initial_state in cases {
647            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
648                .await;
649            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
650                .await;
651        }
652    }
653
654    #[gpui::test]
655    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
656        let mut cx = NeovimBackedTestContext::new(cx)
657            .await
658            .binding(["shift-v", "c"]);
659        cx.assert(indoc! {"
660                The quˇick brown
661                fox jumps over
662                the lazy dog"})
663            .await;
664        // Test pasting code copied on change
665        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
666        cx.assert_state_matches().await;
667
668        cx.assert_all(indoc! {"
669                The quick brown
670                fox juˇmps over
671                the laˇzy dog"})
672            .await;
673        let mut cx = cx.binding(["shift-v", "j", "c"]);
674        cx.assert(indoc! {"
675                The quˇick brown
676                fox jumps over
677                the lazy dog"})
678            .await;
679        // Test pasting code copied on delete
680        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
681        cx.assert_state_matches().await;
682
683        cx.assert_all(indoc! {"
684                The quick brown
685                fox juˇmps over
686                the laˇzy dog"})
687            .await;
688    }
689
690    #[gpui::test]
691    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
692        let cx = VimTestContext::new(cx, true).await;
693        let mut cx = cx.binding(["v", "w", "y"]);
694        cx.assert("The quick ˇbrown", "The quick ˇbrown");
695        cx.assert_clipboard_content(Some("brown"));
696        let mut cx = cx.binding(["v", "w", "j", "y"]);
697        cx.assert(
698            indoc! {"
699                The ˇquick brown
700                fox jumps over
701                the lazy dog"},
702            indoc! {"
703                The ˇquick brown
704                fox jumps over
705                the lazy dog"},
706        );
707        cx.assert_clipboard_content(Some(indoc! {"
708            quick brown
709            fox jumps o"}));
710        cx.assert(
711            indoc! {"
712                The quick brown
713                fox jumps over
714                the ˇlazy dog"},
715            indoc! {"
716                The quick brown
717                fox jumps over
718                the ˇlazy dog"},
719        );
720        cx.assert_clipboard_content(Some("lazy d"));
721        cx.assert(
722            indoc! {"
723                The quick brown
724                fox jumps ˇover
725                the lazy dog"},
726            indoc! {"
727                The quick brown
728                fox jumps ˇover
729                the lazy dog"},
730        );
731        cx.assert_clipboard_content(Some(indoc! {"
732                over
733                t"}));
734        let mut cx = cx.binding(["v", "b", "k", "y"]);
735        cx.assert(
736            indoc! {"
737                The ˇquick brown
738                fox jumps over
739                the lazy dog"},
740            indoc! {"
741                ˇThe quick brown
742                fox jumps over
743                the lazy dog"},
744        );
745        cx.assert_clipboard_content(Some("The q"));
746        cx.assert(
747            indoc! {"
748                The quick brown
749                fox jumps over
750                the ˇlazy dog"},
751            indoc! {"
752                The quick brown
753                ˇfox jumps over
754                the lazy dog"},
755        );
756        cx.assert_clipboard_content(Some(indoc! {"
757            fox jumps over
758            the l"}));
759        cx.assert(
760            indoc! {"
761                The quick brown
762                fox jumps ˇover
763                the lazy dog"},
764            indoc! {"
765                The ˇquick brown
766                fox jumps over
767                the lazy dog"},
768        );
769        cx.assert_clipboard_content(Some(indoc! {"
770            quick brown
771            fox jumps o"}));
772    }
773
774    #[gpui::test]
775    async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
776        let mut cx = VimTestContext::new(cx, true).await;
777        cx.set_state(
778            indoc! {"
779                The quick brown
780                fox «jumpsˇ» over
781                the lazy dog"},
782            Mode::Visual { line: false },
783        );
784        cx.simulate_keystroke("y");
785        cx.set_state(
786            indoc! {"
787                The quick brown
788                fox jumpˇs over
789                the lazy dog"},
790            Mode::Normal,
791        );
792        cx.simulate_keystroke("p");
793        cx.assert_state(
794            indoc! {"
795                The quick brown
796                fox jumpsjumpˇs over
797                the lazy dog"},
798            Mode::Normal,
799        );
800
801        cx.set_state(
802            indoc! {"
803                The quick brown
804                fox ju«mˇ»ps over
805                the lazy dog"},
806            Mode::Visual { line: true },
807        );
808        cx.simulate_keystroke("d");
809        cx.assert_state(
810            indoc! {"
811                The quick brown
812                the laˇzy dog"},
813            Mode::Normal,
814        );
815        cx.set_state(
816            indoc! {"
817                The quick brown
818                the «lazyˇ» dog"},
819            Mode::Visual { line: false },
820        );
821        cx.simulate_keystroke("p");
822        cx.assert_state(
823            &indoc! {"
824                The quick brown
825                the_
826                ˇfox jumps over
827                dog"}
828            .replace("_", " "), // Hack for trailing whitespace
829            Mode::Normal,
830        );
831    }
832}