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: 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 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.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
316pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut WindowContext) {
317 Vim::update(cx, |vim, cx| {
318 vim.update_active_editor(cx, |editor, cx| {
319 editor.transact(cx, |editor, cx| {
320 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
321
322 // Selections are biased right at the start. So we need to store
323 // anchors that are biased left so that we can restore the selections
324 // after the change
325 let stable_anchors = editor
326 .selections
327 .disjoint_anchors()
328 .into_iter()
329 .map(|selection| {
330 let start = selection.start.bias_left(&display_map.buffer_snapshot);
331 start..start
332 })
333 .collect::<Vec<_>>();
334
335 let mut edits = Vec::new();
336 for selection in selections.iter() {
337 let mut selection = selection.clone();
338 if !line && !selection.reversed {
339 // Head is at the end of the selection. Adjust the end position to
340 // to include the character under the cursor.
341 *selection.end.column_mut() = selection.end.column() + 1;
342 selection.end = display_map.clip_point(selection.end, Bias::Right);
343 }
344
345 for row_range in
346 movement::split_display_range_by_lines(&display_map, selection.range())
347 {
348 let range = row_range.start.to_offset(&display_map, Bias::Right)
349 ..row_range.end.to_offset(&display_map, Bias::Right);
350 let text = text.repeat(range.len());
351 edits.push((range, text));
352 }
353 }
354
355 editor.buffer().update(cx, |buffer, cx| {
356 buffer.edit(edits, None, cx);
357 });
358 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
359 });
360 });
361 vim.switch_mode(Mode::Normal, false, cx);
362 });
363}
364
365#[cfg(test)]
366mod test {
367 use indoc::indoc;
368
369 use crate::{
370 state::Mode,
371 test::{NeovimBackedTestContext, VimTestContext},
372 };
373
374 #[gpui::test]
375 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
376 let mut cx = NeovimBackedTestContext::new(cx)
377 .await
378 .binding(["v", "w", "j"]);
379 cx.assert_all(indoc! {"
380 The ˇquick brown
381 fox jumps ˇover
382 the ˇlazy dog"})
383 .await;
384 let mut cx = cx.binding(["v", "b", "k"]);
385 cx.assert_all(indoc! {"
386 The ˇquick brown
387 fox jumps ˇover
388 the ˇlazy dog"})
389 .await;
390 }
391
392 #[gpui::test]
393 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
394 let mut cx = NeovimBackedTestContext::new(cx).await;
395
396 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
397 .await;
398 cx.assert_binding_matches(
399 ["v", "w", "j", "x"],
400 indoc! {"
401 The ˇquick brown
402 fox jumps over
403 the lazy dog"},
404 )
405 .await;
406 // Test pasting code copied on delete
407 cx.simulate_shared_keystrokes(["j", "p"]).await;
408 cx.assert_state_matches().await;
409
410 let mut cx = cx.binding(["v", "w", "j", "x"]);
411 cx.assert_all(indoc! {"
412 The ˇquick brown
413 fox jumps over
414 the ˇlazy dog"})
415 .await;
416 let mut cx = cx.binding(["v", "b", "k", "x"]);
417 cx.assert_all(indoc! {"
418 The ˇquick brown
419 fox jumps ˇover
420 the ˇlazy dog"})
421 .await;
422 }
423
424 #[gpui::test]
425 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
426 let mut cx = NeovimBackedTestContext::new(cx)
427 .await
428 .binding(["shift-v", "x"]);
429 cx.assert(indoc! {"
430 The quˇick brown
431 fox jumps over
432 the lazy dog"})
433 .await;
434 // Test pasting code copied on delete
435 cx.simulate_shared_keystroke("p").await;
436 cx.assert_state_matches().await;
437
438 cx.assert_all(indoc! {"
439 The quick brown
440 fox juˇmps over
441 the laˇzy dog"})
442 .await;
443 let mut cx = cx.binding(["shift-v", "j", "x"]);
444 cx.assert(indoc! {"
445 The quˇick brown
446 fox jumps over
447 the lazy dog"})
448 .await;
449 // Test pasting code copied on delete
450 cx.simulate_shared_keystroke("p").await;
451 cx.assert_state_matches().await;
452
453 cx.assert_all(indoc! {"
454 The quick brown
455 fox juˇmps over
456 the laˇzy dog"})
457 .await;
458 }
459
460 #[gpui::test]
461 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
462 let mut cx = NeovimBackedTestContext::new(cx)
463 .await
464 .binding(["v", "w", "c"]);
465 cx.assert("The quick ˇbrown").await;
466 let mut cx = cx.binding(["v", "w", "j", "c"]);
467 cx.assert_all(indoc! {"
468 The ˇquick brown
469 fox jumps ˇover
470 the ˇlazy dog"})
471 .await;
472 let mut cx = cx.binding(["v", "b", "k", "c"]);
473 cx.assert_all(indoc! {"
474 The ˇquick brown
475 fox jumps ˇover
476 the ˇlazy dog"})
477 .await;
478 }
479
480 #[gpui::test]
481 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
482 let mut cx = NeovimBackedTestContext::new(cx)
483 .await
484 .binding(["shift-v", "c"]);
485 cx.assert(indoc! {"
486 The quˇick brown
487 fox jumps over
488 the lazy dog"})
489 .await;
490 // Test pasting code copied on change
491 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
492 cx.assert_state_matches().await;
493
494 cx.assert_all(indoc! {"
495 The quick brown
496 fox juˇmps over
497 the laˇzy dog"})
498 .await;
499 let mut cx = cx.binding(["shift-v", "j", "c"]);
500 cx.assert(indoc! {"
501 The quˇick brown
502 fox jumps over
503 the lazy dog"})
504 .await;
505 // Test pasting code copied on delete
506 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
507 cx.assert_state_matches().await;
508
509 cx.assert_all(indoc! {"
510 The quick brown
511 fox juˇmps over
512 the laˇzy dog"})
513 .await;
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 jumpsjumpˇs 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 .replace("_", " "), // Hack for trailing whitespace
655 Mode::Normal,
656 );
657 }
658}