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::{
10 motion::Motion,
11 object::Object,
12 state::{Mode, Operator},
13 utils::copy_selections_content,
14 Vim,
15};
16
17actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
18
19pub fn init(cx: &mut MutableAppContext) {
20 cx.add_action(change);
21 cx.add_action(delete);
22 cx.add_action(yank);
23 cx.add_action(paste);
24}
25
26pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
27 Vim::update(cx, |vim, cx| {
28 vim.update_active_editor(cx, |editor, cx| {
29 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
30 s.move_with(|map, selection| {
31 let was_reversed = selection.reversed;
32
33 if let Some((new_head, goal)) =
34 motion.move_point(map, selection.head(), selection.goal, times)
35 {
36 selection.set_head(new_head, goal);
37
38 if was_reversed && !selection.reversed {
39 // Head was at the start of the selection, and now is at the end. We need to move the start
40 // back by one if possible in order to compensate for this change.
41 *selection.start.column_mut() =
42 selection.start.column().saturating_sub(1);
43 selection.start = map.clip_point(selection.start, Bias::Left);
44 } else if !was_reversed && selection.reversed {
45 // Head was at the end of the selection, and now is at the start. We need to move the end
46 // forward by one if possible in order to compensate for this change.
47 *selection.end.column_mut() = selection.end.column() + 1;
48 selection.end = map.clip_point(selection.end, Bias::Right);
49 }
50 }
51 });
52 });
53 });
54 });
55}
56
57pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
58 Vim::update(cx, |vim, cx| {
59 if let Operator::Object { around } = vim.pop_operator(cx) {
60 vim.update_active_editor(cx, |editor, cx| {
61 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
62 s.move_with(|map, selection| {
63 let head = selection.head();
64 if let Some(mut range) = object.range(map, head, around) {
65 if !range.is_empty() {
66 if let Some((_, end)) = map.reverse_chars_at(range.end).next() {
67 range.end = end;
68 }
69
70 if selection.is_empty() {
71 selection.start = range.start;
72 selection.end = range.end;
73 } else if selection.reversed {
74 selection.start = range.start;
75 } else {
76 selection.end = range.end;
77 }
78 }
79 }
80 });
81 });
82 });
83 }
84 });
85}
86
87pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
88 Vim::update(cx, |vim, cx| {
89 vim.update_active_editor(cx, |editor, cx| {
90 editor.set_clip_at_line_ends(false, cx);
91 // Compute edits and resulting anchor selections. If in line mode, adjust
92 // the anchor location and additional newline
93 let mut edits = Vec::new();
94 let mut new_selections = Vec::new();
95 let line_mode = editor.selections.line_mode;
96 editor.change_selections(None, cx, |s| {
97 s.move_with(|map, selection| {
98 if !selection.reversed {
99 // Head is at the end of the selection. Adjust the end position to
100 // to include the character under the cursor.
101 *selection.end.column_mut() = selection.end.column() + 1;
102 selection.end = map.clip_point(selection.end, Bias::Right);
103 }
104
105 if line_mode {
106 let range = selection.map(|p| p.to_point(map)).range();
107 let expanded_range = map.expand_to_line(range);
108 // If we are at the last line, the anchor needs to be after the newline so that
109 // it is on a line of its own. Otherwise, the anchor may be after the newline
110 let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
111 map.buffer_snapshot.anchor_after(expanded_range.end)
112 } else {
113 map.buffer_snapshot.anchor_before(expanded_range.start)
114 };
115
116 edits.push((expanded_range, "\n"));
117 new_selections.push(selection.map(|_| anchor.clone()));
118 } else {
119 let range = selection.map(|p| p.to_point(map)).range();
120 let anchor = map.buffer_snapshot.anchor_after(range.end);
121 edits.push((range, ""));
122 new_selections.push(selection.map(|_| anchor.clone()));
123 }
124 selection.goal = SelectionGoal::None;
125 });
126 });
127 copy_selections_content(editor, editor.selections.line_mode, cx);
128 editor.edit_with_autoindent(edits, cx);
129 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
130 s.select_anchors(new_selections);
131 });
132 });
133 vim.switch_mode(Mode::Insert, false, cx);
134 });
135}
136
137pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
138 Vim::update(cx, |vim, cx| {
139 vim.update_active_editor(cx, |editor, cx| {
140 editor.set_clip_at_line_ends(false, cx);
141 let mut original_columns: HashMap<_, _> = Default::default();
142 let line_mode = editor.selections.line_mode;
143 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
144 s.move_with(|map, selection| {
145 if line_mode {
146 original_columns
147 .insert(selection.id, selection.head().to_point(map).column);
148 } else if !selection.reversed {
149 // Head is at the end of the selection. Adjust the end position to
150 // to include the character under the cursor.
151 *selection.end.column_mut() = selection.end.column() + 1;
152 selection.end = map.clip_point(selection.end, Bias::Right);
153 }
154 selection.goal = SelectionGoal::None;
155 });
156 });
157 copy_selections_content(editor, line_mode, cx);
158 editor.insert("", cx);
159
160 // Fixup cursor position after the deletion
161 editor.set_clip_at_line_ends(true, cx);
162 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
163 s.move_with(|map, selection| {
164 let mut cursor = selection.head().to_point(map);
165
166 if let Some(column) = original_columns.get(&selection.id) {
167 cursor.column = *column
168 }
169 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
170 selection.collapse_to(cursor, selection.goal)
171 });
172 });
173 });
174 vim.switch_mode(Mode::Normal, false, cx);
175 });
176}
177
178pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
179 Vim::update(cx, |vim, cx| {
180 vim.update_active_editor(cx, |editor, cx| {
181 editor.set_clip_at_line_ends(false, cx);
182 let line_mode = editor.selections.line_mode;
183 if !line_mode {
184 editor.change_selections(None, cx, |s| {
185 s.move_with(|map, selection| {
186 if !selection.reversed {
187 // Head is at the end of the selection. Adjust the end position to
188 // to include the character under the cursor.
189 *selection.end.column_mut() = selection.end.column() + 1;
190 selection.end = map.clip_point(selection.end, Bias::Right);
191 }
192 });
193 });
194 }
195 copy_selections_content(editor, line_mode, cx);
196 editor.change_selections(None, cx, |s| {
197 s.move_with(|_, selection| {
198 selection.collapse_to(selection.start, SelectionGoal::None)
199 });
200 });
201 });
202 vim.switch_mode(Mode::Normal, false, cx);
203 });
204}
205
206pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
207 Vim::update(cx, |vim, cx| {
208 vim.update_active_editor(cx, |editor, cx| {
209 editor.transact(cx, |editor, cx| {
210 if let Some(item) = cx.as_mut().read_from_clipboard() {
211 copy_selections_content(editor, editor.selections.line_mode, cx);
212 let mut clipboard_text = Cow::Borrowed(item.text());
213 if let Some(mut clipboard_selections) =
214 item.metadata::<Vec<ClipboardSelection>>()
215 {
216 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
217 let all_selections_were_entire_line =
218 clipboard_selections.iter().all(|s| s.is_entire_line);
219 if clipboard_selections.len() != selections.len() {
220 let mut newline_separated_text = String::new();
221 let mut clipboard_selections =
222 clipboard_selections.drain(..).peekable();
223 let mut ix = 0;
224 while let Some(clipboard_selection) = clipboard_selections.next() {
225 newline_separated_text
226 .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
227 ix += clipboard_selection.len;
228 if clipboard_selections.peek().is_some() {
229 newline_separated_text.push('\n');
230 }
231 }
232 clipboard_text = Cow::Owned(newline_separated_text);
233 }
234
235 let mut new_selections = Vec::new();
236 editor.buffer().update(cx, |buffer, cx| {
237 let snapshot = buffer.snapshot(cx);
238 let mut start_offset = 0;
239 let mut edits = Vec::new();
240 for (ix, selection) in selections.iter().enumerate() {
241 let to_insert;
242 let linewise;
243 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
244 let end_offset = start_offset + clipboard_selection.len;
245 to_insert = &clipboard_text[start_offset..end_offset];
246 linewise = clipboard_selection.is_entire_line;
247 start_offset = end_offset;
248 } else {
249 to_insert = clipboard_text.as_str();
250 linewise = all_selections_were_entire_line;
251 }
252
253 let mut selection = selection.clone();
254 if !selection.reversed {
255 let mut adjusted = selection.end;
256 // Head is at the end of the selection. Adjust the end position to
257 // to include the character under the cursor.
258 *adjusted.column_mut() = adjusted.column() + 1;
259 adjusted = display_map.clip_point(adjusted, Bias::Right);
260 // If the selection is empty, move both the start and end forward one
261 // character
262 if selection.is_empty() {
263 selection.start = adjusted;
264 selection.end = adjusted;
265 } else {
266 selection.end = adjusted;
267 }
268 }
269
270 let range = selection.map(|p| p.to_point(&display_map)).range();
271
272 let new_position = if linewise {
273 edits.push((range.start..range.start, "\n"));
274 let mut new_position = range.start;
275 new_position.column = 0;
276 new_position.row += 1;
277 new_position
278 } else {
279 range.start
280 };
281
282 new_selections.push(selection.map(|_| new_position));
283
284 if linewise && to_insert.ends_with('\n') {
285 edits.push((
286 range.clone(),
287 &to_insert[0..to_insert.len().saturating_sub(1)],
288 ))
289 } else {
290 edits.push((range.clone(), to_insert));
291 }
292
293 if linewise {
294 edits.push((range.end..range.end, "\n"));
295 }
296 }
297 drop(snapshot);
298 buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
299 });
300
301 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
302 s.select(new_selections)
303 });
304 } else {
305 editor.insert(&clipboard_text, cx);
306 }
307 }
308 });
309 });
310 vim.switch_mode(Mode::Normal, false, cx);
311 });
312}
313
314#[cfg(test)]
315mod test {
316 use indoc::indoc;
317
318 use crate::{
319 state::Mode,
320 test::{NeovimBackedTestContext, VimTestContext},
321 };
322
323 #[gpui::test]
324 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
325 let mut cx = NeovimBackedTestContext::new(cx)
326 .await
327 .binding(["v", "w", "j"]);
328 cx.assert_all(indoc! {"
329 The ˇquick brown
330 fox jumps ˇover
331 the ˇlazy dog"})
332 .await;
333 let mut cx = cx.binding(["v", "b", "k"]);
334 cx.assert_all(indoc! {"
335 The ˇquick brown
336 fox jumps ˇover
337 the ˇlazy dog"})
338 .await;
339 }
340
341 #[gpui::test]
342 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
343 let mut cx = NeovimBackedTestContext::new(cx).await;
344
345 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
346 .await;
347 cx.assert_binding_matches(
348 ["v", "w", "j", "x"],
349 indoc! {"
350 The ˇquick brown
351 fox jumps over
352 the lazy dog"},
353 )
354 .await;
355 // Test pasting code copied on delete
356 cx.simulate_shared_keystrokes(["j", "p"]).await;
357 cx.assert_state_matches().await;
358
359 let mut cx = cx.binding(["v", "w", "j", "x"]);
360 cx.assert_all(indoc! {"
361 The ˇquick brown
362 fox jumps over
363 the ˇlazy dog"})
364 .await;
365 let mut cx = cx.binding(["v", "b", "k", "x"]);
366 cx.assert_all(indoc! {"
367 The ˇquick brown
368 fox jumps ˇover
369 the ˇlazy dog"})
370 .await;
371 }
372
373 #[gpui::test]
374 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
375 let mut cx = NeovimBackedTestContext::new(cx)
376 .await
377 .binding(["shift-v", "x"]);
378 cx.assert(indoc! {"
379 The quˇick brown
380 fox jumps over
381 the lazy dog"})
382 .await;
383 // Test pasting code copied on delete
384 cx.simulate_shared_keystroke("p").await;
385 cx.assert_state_matches().await;
386
387 cx.assert_all(indoc! {"
388 The quick brown
389 fox juˇmps over
390 the laˇzy dog"})
391 .await;
392 let mut cx = cx.binding(["shift-v", "j", "x"]);
393 cx.assert(indoc! {"
394 The quˇick brown
395 fox jumps over
396 the lazy dog"})
397 .await;
398 // Test pasting code copied on delete
399 cx.simulate_shared_keystroke("p").await;
400 cx.assert_state_matches().await;
401
402 cx.assert_all(indoc! {"
403 The quick brown
404 fox juˇmps over
405 the laˇzy dog"})
406 .await;
407 }
408
409 #[gpui::test]
410 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
411 let mut cx = NeovimBackedTestContext::new(cx)
412 .await
413 .binding(["v", "w", "c"]);
414 cx.assert("The quick ˇbrown").await;
415 let mut cx = cx.binding(["v", "w", "j", "c"]);
416 cx.assert_all(indoc! {"
417 The ˇquick brown
418 fox jumps ˇover
419 the ˇlazy dog"})
420 .await;
421 let mut cx = cx.binding(["v", "b", "k", "c"]);
422 cx.assert_all(indoc! {"
423 The ˇquick brown
424 fox jumps ˇover
425 the ˇlazy dog"})
426 .await;
427 }
428
429 #[gpui::test]
430 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
431 let mut cx = NeovimBackedTestContext::new(cx)
432 .await
433 .binding(["shift-v", "c"]);
434 cx.assert(indoc! {"
435 The quˇick brown
436 fox jumps over
437 the lazy dog"})
438 .await;
439 // Test pasting code copied on change
440 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
441 cx.assert_state_matches().await;
442
443 cx.assert_all(indoc! {"
444 The quick brown
445 fox juˇmps over
446 the laˇzy dog"})
447 .await;
448 let mut cx = cx.binding(["shift-v", "j", "c"]);
449 cx.assert(indoc! {"
450 The quˇick brown
451 fox jumps over
452 the lazy dog"})
453 .await;
454 // Test pasting code copied on delete
455 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
456 cx.assert_state_matches().await;
457
458 cx.assert_all(indoc! {"
459 The quick brown
460 fox juˇmps over
461 the laˇzy dog"})
462 .await;
463 }
464
465 #[gpui::test]
466 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
467 let cx = VimTestContext::new(cx, true).await;
468 let mut cx = cx.binding(["v", "w", "y"]);
469 cx.assert("The quick ˇbrown", "The quick ˇbrown");
470 cx.assert_clipboard_content(Some("brown"));
471 let mut cx = cx.binding(["v", "w", "j", "y"]);
472 cx.assert(
473 indoc! {"
474 The ˇquick brown
475 fox jumps over
476 the lazy dog"},
477 indoc! {"
478 The ˇquick brown
479 fox jumps over
480 the lazy dog"},
481 );
482 cx.assert_clipboard_content(Some(indoc! {"
483 quick brown
484 fox jumps o"}));
485 cx.assert(
486 indoc! {"
487 The quick brown
488 fox jumps over
489 the ˇlazy dog"},
490 indoc! {"
491 The quick brown
492 fox jumps over
493 the ˇlazy dog"},
494 );
495 cx.assert_clipboard_content(Some("lazy d"));
496 cx.assert(
497 indoc! {"
498 The quick brown
499 fox jumps ˇover
500 the lazy dog"},
501 indoc! {"
502 The quick brown
503 fox jumps ˇover
504 the lazy dog"},
505 );
506 cx.assert_clipboard_content(Some(indoc! {"
507 over
508 t"}));
509 let mut cx = cx.binding(["v", "b", "k", "y"]);
510 cx.assert(
511 indoc! {"
512 The ˇquick brown
513 fox jumps over
514 the lazy dog"},
515 indoc! {"
516 ˇThe quick brown
517 fox jumps over
518 the lazy dog"},
519 );
520 cx.assert_clipboard_content(Some("The q"));
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(indoc! {"
532 fox jumps over
533 the l"}));
534 cx.assert(
535 indoc! {"
536 The quick brown
537 fox jumps ˇover
538 the lazy dog"},
539 indoc! {"
540 The ˇquick brown
541 fox jumps over
542 the lazy dog"},
543 );
544 cx.assert_clipboard_content(Some(indoc! {"
545 quick brown
546 fox jumps o"}));
547 }
548
549 #[gpui::test]
550 async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
551 let mut cx = VimTestContext::new(cx, true).await;
552 cx.set_state(
553 indoc! {"
554 The quick brown
555 fox «jumpˇ»s over
556 the lazy dog"},
557 Mode::Visual { line: false },
558 );
559 cx.simulate_keystroke("y");
560 cx.set_state(
561 indoc! {"
562 The quick brown
563 fox jumpˇs over
564 the lazy dog"},
565 Mode::Normal,
566 );
567 cx.simulate_keystroke("p");
568 cx.assert_state(
569 indoc! {"
570 The quick brown
571 fox jumpsjumpˇs over
572 the lazy dog"},
573 Mode::Normal,
574 );
575
576 cx.set_state(
577 indoc! {"
578 The quick brown
579 fox juˇmps over
580 the lazy dog"},
581 Mode::Visual { line: true },
582 );
583 cx.simulate_keystroke("d");
584 cx.assert_state(
585 indoc! {"
586 The quick brown
587 the laˇzy dog"},
588 Mode::Normal,
589 );
590 cx.set_state(
591 indoc! {"
592 The quick brown
593 the «lazˇ»y dog"},
594 Mode::Visual { line: false },
595 );
596 cx.simulate_keystroke("p");
597 cx.assert_state(
598 indoc! {"
599 The quick brown
600 the
601 ˇfox jumps over
602 dog"},
603 Mode::Normal,
604 );
605 }
606}