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
316pub(crate) fn visual_replace(text: &str, line: bool, cx: &mut MutableAppContext) {
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 let mut new_selections = Vec::new();
322 editor.buffer().update(cx, |buffer, cx| {
323 let mut edits = Vec::new();
324 for selection in selections.iter() {
325 let mut selection = selection.clone();
326 if !line && !selection.reversed {
327 // Head is at the end of the selection. Adjust the end position to
328 // to include the character under the cursor.
329 *selection.end.column_mut() = selection.end.column() + 1;
330 selection.end = display_map.clip_point(selection.end, Bias::Right);
331 }
332
333 let range = selection
334 .map(|p| p.to_offset(&display_map, Bias::Right))
335 .range();
336 new_selections.push(range.start..range.start);
337 let text = text.repeat(range.len());
338 edits.push((range, text));
339 }
340
341 buffer.edit(edits, None, cx);
342 });
343 editor.change_selections(None, cx, |s| s.select_ranges(new_selections));
344 });
345 });
346 vim.pop_operator(cx)
347 });
348}
349
350#[cfg(test)]
351mod test {
352 use indoc::indoc;
353
354 use crate::{
355 state::Mode,
356 test::{NeovimBackedTestContext, VimTestContext},
357 };
358
359 #[gpui::test]
360 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
361 let mut cx = NeovimBackedTestContext::new(cx)
362 .await
363 .binding(["v", "w", "j"]);
364 cx.assert_all(indoc! {"
365 The ˇquick brown
366 fox jumps ˇover
367 the ˇlazy dog"})
368 .await;
369 let mut cx = cx.binding(["v", "b", "k"]);
370 cx.assert_all(indoc! {"
371 The ˇquick brown
372 fox jumps ˇover
373 the ˇlazy dog"})
374 .await;
375 }
376
377 #[gpui::test]
378 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
379 let mut cx = NeovimBackedTestContext::new(cx).await;
380
381 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
382 .await;
383 cx.assert_binding_matches(
384 ["v", "w", "j", "x"],
385 indoc! {"
386 The ˇquick brown
387 fox jumps over
388 the lazy dog"},
389 )
390 .await;
391 // Test pasting code copied on delete
392 cx.simulate_shared_keystrokes(["j", "p"]).await;
393 cx.assert_state_matches().await;
394
395 let mut cx = cx.binding(["v", "w", "j", "x"]);
396 cx.assert_all(indoc! {"
397 The ˇquick brown
398 fox jumps over
399 the ˇlazy dog"})
400 .await;
401 let mut cx = cx.binding(["v", "b", "k", "x"]);
402 cx.assert_all(indoc! {"
403 The ˇquick brown
404 fox jumps ˇover
405 the ˇlazy dog"})
406 .await;
407 }
408
409 #[gpui::test]
410 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
411 let mut cx = NeovimBackedTestContext::new(cx)
412 .await
413 .binding(["shift-v", "x"]);
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_keystroke("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 let mut cx = cx.binding(["shift-v", "j", "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 }
444
445 #[gpui::test]
446 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
447 let mut cx = NeovimBackedTestContext::new(cx)
448 .await
449 .binding(["v", "w", "c"]);
450 cx.assert("The quick ˇbrown").await;
451 let mut cx = cx.binding(["v", "w", "j", "c"]);
452 cx.assert_all(indoc! {"
453 The ˇquick brown
454 fox jumps ˇover
455 the ˇlazy dog"})
456 .await;
457 let mut cx = cx.binding(["v", "b", "k", "c"]);
458 cx.assert_all(indoc! {"
459 The ˇquick brown
460 fox jumps ˇover
461 the ˇlazy dog"})
462 .await;
463 }
464
465 #[gpui::test]
466 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
467 let mut cx = NeovimBackedTestContext::new(cx)
468 .await
469 .binding(["shift-v", "c"]);
470 cx.assert(indoc! {"
471 The quˇick brown
472 fox jumps over
473 the lazy dog"})
474 .await;
475 // Test pasting code copied on change
476 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
477 cx.assert_state_matches().await;
478
479 cx.assert_all(indoc! {"
480 The quick brown
481 fox juˇmps over
482 the laˇzy dog"})
483 .await;
484 let mut cx = cx.binding(["shift-v", "j", "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 delete
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 }
500
501 #[gpui::test]
502 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
503 let cx = VimTestContext::new(cx, true).await;
504 let mut cx = cx.binding(["v", "w", "y"]);
505 cx.assert("The quick ˇbrown", "The quick ˇbrown");
506 cx.assert_clipboard_content(Some("brown"));
507 let mut cx = cx.binding(["v", "w", "j", "y"]);
508 cx.assert(
509 indoc! {"
510 The ˇquick brown
511 fox jumps over
512 the lazy dog"},
513 indoc! {"
514 The ˇquick brown
515 fox jumps over
516 the lazy dog"},
517 );
518 cx.assert_clipboard_content(Some(indoc! {"
519 quick brown
520 fox jumps o"}));
521 cx.assert(
522 indoc! {"
523 The quick brown
524 fox jumps over
525 the ˇlazy dog"},
526 indoc! {"
527 The quick brown
528 fox jumps over
529 the ˇlazy dog"},
530 );
531 cx.assert_clipboard_content(Some("lazy d"));
532 cx.assert(
533 indoc! {"
534 The quick brown
535 fox jumps ˇover
536 the lazy dog"},
537 indoc! {"
538 The quick brown
539 fox jumps ˇover
540 the lazy dog"},
541 );
542 cx.assert_clipboard_content(Some(indoc! {"
543 over
544 t"}));
545 let mut cx = cx.binding(["v", "b", "k", "y"]);
546 cx.assert(
547 indoc! {"
548 The ˇquick brown
549 fox jumps over
550 the lazy dog"},
551 indoc! {"
552 ˇThe quick brown
553 fox jumps over
554 the lazy dog"},
555 );
556 cx.assert_clipboard_content(Some("The q"));
557 cx.assert(
558 indoc! {"
559 The quick brown
560 fox jumps over
561 the ˇlazy dog"},
562 indoc! {"
563 The quick brown
564 ˇfox jumps over
565 the lazy dog"},
566 );
567 cx.assert_clipboard_content(Some(indoc! {"
568 fox jumps over
569 the l"}));
570 cx.assert(
571 indoc! {"
572 The quick brown
573 fox jumps ˇover
574 the lazy dog"},
575 indoc! {"
576 The ˇquick brown
577 fox jumps over
578 the lazy dog"},
579 );
580 cx.assert_clipboard_content(Some(indoc! {"
581 quick brown
582 fox jumps o"}));
583 }
584
585 #[gpui::test]
586 async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
587 let mut cx = VimTestContext::new(cx, true).await;
588 cx.set_state(
589 indoc! {"
590 The quick brown
591 fox «jumpˇ»s over
592 the lazy dog"},
593 Mode::Visual { line: false },
594 );
595 cx.simulate_keystroke("y");
596 cx.set_state(
597 indoc! {"
598 The quick brown
599 fox jumpˇs over
600 the lazy dog"},
601 Mode::Normal,
602 );
603 cx.simulate_keystroke("p");
604 cx.assert_state(
605 indoc! {"
606 The quick brown
607 fox jumpsjumpˇs over
608 the lazy dog"},
609 Mode::Normal,
610 );
611
612 cx.set_state(
613 indoc! {"
614 The quick brown
615 fox juˇmps over
616 the lazy dog"},
617 Mode::Visual { line: true },
618 );
619 cx.simulate_keystroke("d");
620 cx.assert_state(
621 indoc! {"
622 The quick brown
623 the laˇzy dog"},
624 Mode::Normal,
625 );
626 cx.set_state(
627 indoc! {"
628 The quick brown
629 the «lazˇ»y dog"},
630 Mode::Visual { line: false },
631 );
632 cx.simulate_keystroke("p");
633 cx.assert_state(
634 indoc! {"
635 The quick brown
636 the
637 ˇfox jumps over
638 dog"},
639 Mode::Normal,
640 );
641 }
642}