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::{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).await;
309
310        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
311            .await;
312        cx.assert_binding_matches(
313            ["v", "w", "j", "x"],
314            indoc! {"
315                The ˇquick brown
316                fox jumps over
317                the lazy dog"},
318        )
319        .await;
320        // Test pasting code copied on delete
321        cx.simulate_shared_keystrokes(["j", "p"]).await;
322        cx.assert_state_matches().await;
323
324        let mut cx = cx.binding(["v", "w", "j", "x"]);
325        cx.assert_all(indoc! {"
326                The ˇquick brown
327                fox jumps over
328                the ˇlazy dog"})
329            .await;
330        let mut cx = cx.binding(["v", "b", "k", "x"]);
331        cx.assert_all(indoc! {"
332                The ˇquick brown
333                fox jumps ˇover
334                the ˇlazy dog"})
335            .await;
336    }
337
338    #[gpui::test]
339    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
340        let mut cx = NeovimBackedTestContext::new(cx)
341            .await
342            .binding(["shift-v", "x"]);
343        cx.assert(indoc! {"
344                The quˇick brown
345                fox jumps over
346                the lazy dog"})
347            .await;
348        // Test pasting code copied on delete
349        cx.simulate_shared_keystroke("p").await;
350        cx.assert_state_matches().await;
351
352        cx.assert_all(indoc! {"
353                The quick brown
354                fox juˇmps over
355                the laˇzy dog"})
356            .await;
357        let mut cx = cx.binding(["shift-v", "j", "x"]);
358        cx.assert(indoc! {"
359                The quˇick brown
360                fox jumps over
361                the lazy dog"})
362            .await;
363        // Test pasting code copied on delete
364        cx.simulate_shared_keystroke("p").await;
365        cx.assert_state_matches().await;
366
367        cx.assert_all(indoc! {"
368                The quick brown
369                fox juˇmps over
370                the laˇzy dog"})
371            .await;
372    }
373
374    #[gpui::test]
375    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
376        let mut cx = NeovimBackedTestContext::new(cx)
377            .await
378            .binding(["v", "w", "c"]);
379        cx.assert("The quick ˇbrown").await;
380        let mut cx = cx.binding(["v", "w", "j", "c"]);
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", "c"]);
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_line_change(cx: &mut gpui::TestAppContext) {
396        let mut cx = NeovimBackedTestContext::new(cx)
397            .await
398            .binding(["shift-v", "c"]);
399        cx.assert(indoc! {"
400                The quˇick brown
401                fox jumps over
402                the lazy dog"})
403            .await;
404        // Test pasting code copied on change
405        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
406        cx.assert_state_matches().await;
407
408        cx.assert_all(indoc! {"
409                The quick brown
410                fox juˇmps over
411                the laˇzy dog"})
412            .await;
413        let mut cx = cx.binding(["shift-v", "j", "c"]);
414        cx.assert(indoc! {"
415                The quˇick brown
416                fox jumps over
417                the lazy dog"})
418            .await;
419        // Test pasting code copied on delete
420        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
421        cx.assert_state_matches().await;
422
423        cx.assert_all(indoc! {"
424                The quick brown
425                fox juˇmps over
426                the laˇzy dog"})
427            .await;
428    }
429
430    #[gpui::test]
431    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
432        let cx = VimTestContext::new(cx, true).await;
433        let mut cx = cx.binding(["v", "w", "y"]);
434        cx.assert("The quick ˇbrown", "The quick ˇbrown");
435        cx.assert_clipboard_content(Some("brown"));
436        let mut cx = cx.binding(["v", "w", "j", "y"]);
437        cx.assert(
438            indoc! {"
439                The ˇquick brown
440                fox jumps over
441                the lazy dog"},
442            indoc! {"
443                The ˇquick brown
444                fox jumps over
445                the lazy dog"},
446        );
447        cx.assert_clipboard_content(Some(indoc! {"
448            quick brown
449            fox jumps o"}));
450        cx.assert(
451            indoc! {"
452                The quick brown
453                fox jumps over
454                the ˇlazy dog"},
455            indoc! {"
456                The quick brown
457                fox jumps over
458                the ˇlazy dog"},
459        );
460        cx.assert_clipboard_content(Some("lazy d"));
461        cx.assert(
462            indoc! {"
463                The quick brown
464                fox jumps ˇover
465                the lazy dog"},
466            indoc! {"
467                The quick brown
468                fox jumps ˇover
469                the lazy dog"},
470        );
471        cx.assert_clipboard_content(Some(indoc! {"
472                over
473                t"}));
474        let mut cx = cx.binding(["v", "b", "k", "y"]);
475        cx.assert(
476            indoc! {"
477                The ˇquick brown
478                fox jumps over
479                the lazy dog"},
480            indoc! {"
481                ˇThe quick brown
482                fox jumps over
483                the lazy dog"},
484        );
485        cx.assert_clipboard_content(Some("The q"));
486        cx.assert(
487            indoc! {"
488                The quick brown
489                fox jumps over
490                the ˇlazy dog"},
491            indoc! {"
492                The quick brown
493                ˇfox jumps over
494                the lazy dog"},
495        );
496        cx.assert_clipboard_content(Some(indoc! {"
497            fox jumps over
498            the l"}));
499        cx.assert(
500            indoc! {"
501                The quick brown
502                fox jumps ˇover
503                the lazy dog"},
504            indoc! {"
505                The ˇquick brown
506                fox jumps over
507                the lazy dog"},
508        );
509        cx.assert_clipboard_content(Some(indoc! {"
510            quick brown
511            fox jumps o"}));
512    }
513
514    #[gpui::test]
515    async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
516        let mut cx = VimTestContext::new(cx, true).await;
517        cx.set_state(
518            indoc! {"
519                The quick brown
520                fox «jumpˇ»s over
521                the lazy dog"},
522            Mode::Visual { line: false },
523        );
524        cx.simulate_keystroke("y");
525        cx.set_state(
526            indoc! {"
527                The quick brown
528                fox jumpˇs over
529                the lazy dog"},
530            Mode::Normal,
531        );
532        cx.simulate_keystroke("p");
533        cx.assert_state(
534            indoc! {"
535                The quick brown
536                fox jumpsjumpˇs over
537                the lazy dog"},
538            Mode::Normal,
539        );
540
541        cx.set_state(
542            indoc! {"
543                The quick brown
544                fox juˇmps over
545                the lazy dog"},
546            Mode::Visual { line: true },
547        );
548        cx.simulate_keystroke("d");
549        cx.assert_state(
550            indoc! {"
551                The quick brown
552                the laˇzy dog"},
553            Mode::Normal,
554        );
555        cx.set_state(
556            indoc! {"
557                The quick brown
558                the «lazˇ»y dog"},
559            Mode::Visual { line: false },
560        );
561        cx.simulate_keystroke("p");
562        cx.assert_state(
563            indoc! {"
564                The quick brown
565                the 
566                ˇfox jumps over
567                 dog"},
568            Mode::Normal,
569        );
570    }
571}