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