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