visual.rs

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