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}