visual.rs

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