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::{
284        state::Mode,
285        test_contexts::{NeovimBackedTestContext, VimTestContext},
286    };
287
288    #[gpui::test]
289    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
290        let mut cx = NeovimBackedTestContext::new(cx)
291            .await
292            .binding(["v", "w", "j"]);
293        cx.assert_all(indoc! {"
294                The ˇquick brown
295                fox jumps ˇover
296                the ˇlazy dog"})
297            .await;
298        let mut cx = cx.binding(["v", "b", "k"]);
299        cx.assert_all(indoc! {"
300                The ˇquick brown
301                fox jumps ˇover
302                the ˇlazy dog"})
303            .await;
304    }
305
306    #[gpui::test]
307    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
308        let mut cx = NeovimBackedTestContext::new(cx)
309            .await
310            .binding(["v", "w", "x"]);
311        cx.assert("The quick ˇbrown").await;
312        let mut cx = cx.binding(["v", "w", "j", "x"]);
313        cx.assert(indoc! {"
314                The ˇquick brown
315                fox jumps over
316                the lazy dog"})
317            .await;
318        // Test pasting code copied on delete
319        cx.simulate_shared_keystrokes(["j", "p"]).await;
320        cx.assert_state_matches().await;
321
322        cx.assert_all(indoc! {"
323                The ˇquick brown
324                fox jumps over
325                the ˇlazy dog"})
326            .await;
327        let mut cx = cx.binding(["v", "b", "k", "x"]);
328        cx.assert_all(indoc! {"
329                The ˇquick brown
330                fox jumps ˇover
331                the ˇlazy dog"})
332            .await;
333    }
334
335    #[gpui::test]
336    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
337        let mut cx = NeovimBackedTestContext::new(cx)
338            .await
339            .binding(["shift-v", "x"]);
340        cx.assert(indoc! {"
341                The quˇick brown
342                fox jumps over
343                the lazy dog"})
344            .await;
345        // Test pasting code copied on delete
346        cx.simulate_shared_keystroke("p").await;
347        cx.assert_state_matches().await;
348
349        cx.assert_all(indoc! {"
350                The quick brown
351                fox juˇmps over
352                the laˇzy dog"})
353            .await;
354        let mut cx = cx.binding(["shift-v", "j", "x"]);
355        cx.assert(indoc! {"
356                The quˇick brown
357                fox jumps over
358                the lazy dog"})
359            .await;
360        // Test pasting code copied on delete
361        cx.simulate_shared_keystroke("p").await;
362        cx.assert_state_matches().await;
363
364        cx.assert_all(indoc! {"
365                The quick brown
366                fox juˇmps over
367                the laˇzy dog"})
368            .await;
369    }
370
371    #[gpui::test]
372    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
373        let cx = VimTestContext::new(cx, true).await;
374        let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
375        cx.assert("The quick ˇbrown", "The quick ˇ");
376        let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
377        cx.assert(
378            indoc! {"
379                The ˇquick brown
380                fox jumps over
381                the lazy dog"},
382            indoc! {"
383                The ˇver
384                the lazy dog"},
385        );
386        cx.assert(
387            indoc! {"
388                The quick brown
389                fox jumps over
390                the ˇlazy dog"},
391            indoc! {"
392                The quick brown
393                fox jumps over
394                the ˇog"},
395        );
396        cx.assert(
397            indoc! {"
398                The quick brown
399                fox jumps ˇover
400                the lazy dog"},
401            indoc! {"
402                The quick brown
403                fox jumps ˇhe lazy dog"},
404        );
405        let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
406        cx.assert(
407            indoc! {"
408                The ˇquick brown
409                fox jumps over
410                the lazy dog"},
411            indoc! {"
412                ˇuick brown
413                fox jumps over
414                the lazy dog"},
415        );
416        cx.assert(
417            indoc! {"
418                The quick brown
419                fox jumps over
420                the ˇlazy dog"},
421            indoc! {"
422                The quick brown
423                ˇazy dog"},
424        );
425        cx.assert(
426            indoc! {"
427                The quick brown
428                fox jumps ˇover
429                the lazy dog"},
430            indoc! {"
431                The ˇver
432                the lazy dog"},
433        );
434    }
435
436    #[gpui::test]
437    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
438        let cx = VimTestContext::new(cx, true).await;
439        let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
440        cx.assert(
441            indoc! {"
442                The quˇick brown
443                fox jumps over
444                the lazy dog"},
445            indoc! {"
446                ˇ
447                fox jumps over
448                the lazy dog"},
449        );
450        // Test pasting code copied on change
451        cx.simulate_keystrokes(["escape", "j", "p"]);
452        cx.assert_editor_state(indoc! {"
453            
454            fox jumps over
455            ˇThe quick brown
456            the lazy dog"});
457
458        cx.assert(
459            indoc! {"
460                The quick brown
461                fox juˇmps over
462                the lazy dog"},
463            indoc! {"
464                The quick brown
465                ˇ
466                the lazy dog"},
467        );
468        cx.assert(
469            indoc! {"
470                The quick brown
471                fox jumps over
472                the laˇzy dog"},
473            indoc! {"
474                The quick brown
475                fox jumps over
476                ˇ"},
477        );
478        let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
479        cx.assert(
480            indoc! {"
481                The quˇick brown
482                fox jumps over
483                the lazy dog"},
484            indoc! {"
485                ˇ
486                the lazy dog"},
487        );
488        // Test pasting code copied on delete
489        cx.simulate_keystrokes(["escape", "j", "p"]);
490        cx.assert_editor_state(indoc! {"
491            
492            the lazy dog
493            ˇThe quick brown
494            fox jumps over"});
495        cx.assert(
496            indoc! {"
497                The quick brown
498                fox juˇmps over
499                the lazy dog"},
500            indoc! {"
501                The quick brown
502                ˇ"},
503        );
504        cx.assert(
505            indoc! {"
506                The quick brown
507                fox jumps over
508                the laˇzy dog"},
509            indoc! {"
510                The quick brown
511                fox jumps over
512                ˇ"},
513        );
514    }
515
516    #[gpui::test]
517    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
518        let cx = VimTestContext::new(cx, true).await;
519        let mut cx = cx.binding(["v", "w", "y"]);
520        cx.assert("The quick ˇbrown", "The quick ˇbrown");
521        cx.assert_clipboard_content(Some("brown"));
522        let mut cx = cx.binding(["v", "w", "j", "y"]);
523        cx.assert(
524            indoc! {"
525                The ˇquick brown
526                fox jumps over
527                the lazy dog"},
528            indoc! {"
529                The ˇquick brown
530                fox jumps over
531                the lazy dog"},
532        );
533        cx.assert_clipboard_content(Some(indoc! {"
534            quick brown
535            fox jumps o"}));
536        cx.assert(
537            indoc! {"
538                The quick brown
539                fox jumps over
540                the ˇlazy dog"},
541            indoc! {"
542                The quick brown
543                fox jumps over
544                the ˇlazy dog"},
545        );
546        cx.assert_clipboard_content(Some("lazy d"));
547        cx.assert(
548            indoc! {"
549                The quick brown
550                fox jumps ˇover
551                the lazy dog"},
552            indoc! {"
553                The quick brown
554                fox jumps ˇover
555                the lazy dog"},
556        );
557        cx.assert_clipboard_content(Some(indoc! {"
558                over
559                t"}));
560        let mut cx = cx.binding(["v", "b", "k", "y"]);
561        cx.assert(
562            indoc! {"
563                The ˇquick brown
564                fox jumps over
565                the lazy dog"},
566            indoc! {"
567                ˇThe quick brown
568                fox jumps over
569                the lazy dog"},
570        );
571        cx.assert_clipboard_content(Some("The q"));
572        cx.assert(
573            indoc! {"
574                The quick brown
575                fox jumps over
576                the ˇlazy dog"},
577            indoc! {"
578                The quick brown
579                ˇfox jumps over
580                the lazy dog"},
581        );
582        cx.assert_clipboard_content(Some(indoc! {"
583            fox jumps over
584            the l"}));
585        cx.assert(
586            indoc! {"
587                The quick brown
588                fox jumps ˇover
589                the lazy dog"},
590            indoc! {"
591                The ˇquick brown
592                fox jumps over
593                the lazy dog"},
594        );
595        cx.assert_clipboard_content(Some(indoc! {"
596            quick brown
597            fox jumps o"}));
598    }
599
600    #[gpui::test]
601    async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
602        let mut cx = VimTestContext::new(cx, true).await;
603        cx.set_state(
604            indoc! {"
605                The quick brown
606                fox «jumpˇ»s over
607                the lazy dog"},
608            Mode::Visual { line: false },
609        );
610        cx.simulate_keystroke("y");
611        cx.set_state(
612            indoc! {"
613                The quick brown
614                fox jumpˇs over
615                the lazy dog"},
616            Mode::Normal,
617        );
618        cx.simulate_keystroke("p");
619        cx.assert_state(
620            indoc! {"
621                The quick brown
622                fox jumpsˇjumps over
623                the lazy dog"},
624            Mode::Normal,
625        );
626
627        cx.set_state(
628            indoc! {"
629                The quick brown
630                fox juˇmps over
631                the lazy dog"},
632            Mode::Visual { line: true },
633        );
634        cx.simulate_keystroke("d");
635        cx.assert_state(
636            indoc! {"
637                The quick brown
638                the laˇzy dog"},
639            Mode::Normal,
640        );
641        cx.set_state(
642            indoc! {"
643                The quick brown
644                the «lazˇ»y dog"},
645            Mode::Visual { line: false },
646        );
647        cx.simulate_keystroke("p");
648        cx.assert_state(
649            indoc! {"
650                The quick brown
651                the 
652                ˇfox jumps over
653                 dog"},
654            Mode::Normal,
655        );
656    }
657}