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}