visual.rs

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