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
316#[cfg(test)]
317mod test {
318 use indoc::indoc;
319
320 use crate::{
321 state::Mode,
322 test::{NeovimBackedTestContext, VimTestContext},
323 };
324
325 #[gpui::test]
326 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
327 let mut cx = NeovimBackedTestContext::new(cx)
328 .await
329 .binding(["v", "w", "j"]);
330 cx.assert_all(indoc! {"
331 The ˇquick brown
332 fox jumps ˇover
333 the ˇlazy dog"})
334 .await;
335 let mut cx = cx.binding(["v", "b", "k"]);
336 cx.assert_all(indoc! {"
337 The ˇquick brown
338 fox jumps ˇover
339 the ˇlazy dog"})
340 .await;
341 }
342
343 #[gpui::test]
344 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
345 let mut cx = NeovimBackedTestContext::new(cx).await;
346
347 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
348 .await;
349 cx.assert_binding_matches(
350 ["v", "w", "j", "x"],
351 indoc! {"
352 The ˇquick brown
353 fox jumps over
354 the lazy dog"},
355 )
356 .await;
357 // Test pasting code copied on delete
358 cx.simulate_shared_keystrokes(["j", "p"]).await;
359 cx.assert_state_matches().await;
360
361 let mut cx = cx.binding(["v", "w", "j", "x"]);
362 cx.assert_all(indoc! {"
363 The ˇquick brown
364 fox jumps over
365 the ˇlazy dog"})
366 .await;
367 let mut cx = cx.binding(["v", "b", "k", "x"]);
368 cx.assert_all(indoc! {"
369 The ˇquick brown
370 fox jumps ˇover
371 the ˇlazy dog"})
372 .await;
373 }
374
375 #[gpui::test]
376 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
377 let mut cx = NeovimBackedTestContext::new(cx)
378 .await
379 .binding(["shift-v", "x"]);
380 cx.assert(indoc! {"
381 The quˇick brown
382 fox jumps over
383 the lazy dog"})
384 .await;
385 // Test pasting code copied on delete
386 cx.simulate_shared_keystroke("p").await;
387 cx.assert_state_matches().await;
388
389 cx.assert_all(indoc! {"
390 The quick brown
391 fox juˇmps over
392 the laˇzy dog"})
393 .await;
394 let mut cx = cx.binding(["shift-v", "j", "x"]);
395 cx.assert(indoc! {"
396 The quˇick brown
397 fox jumps over
398 the lazy dog"})
399 .await;
400 // Test pasting code copied on delete
401 cx.simulate_shared_keystroke("p").await;
402 cx.assert_state_matches().await;
403
404 cx.assert_all(indoc! {"
405 The quick brown
406 fox juˇmps over
407 the laˇzy dog"})
408 .await;
409 }
410
411 #[gpui::test]
412 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
413 let mut cx = NeovimBackedTestContext::new(cx)
414 .await
415 .binding(["v", "w", "c"]);
416 cx.assert("The quick ˇbrown").await;
417 let mut cx = cx.binding(["v", "w", "j", "c"]);
418 cx.assert_all(indoc! {"
419 The ˇquick brown
420 fox jumps ˇover
421 the ˇlazy dog"})
422 .await;
423 let mut cx = cx.binding(["v", "b", "k", "c"]);
424 cx.assert_all(indoc! {"
425 The ˇquick brown
426 fox jumps ˇover
427 the ˇlazy dog"})
428 .await;
429 }
430
431 #[gpui::test]
432 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
433 let mut cx = NeovimBackedTestContext::new(cx)
434 .await
435 .binding(["shift-v", "c"]);
436 cx.assert(indoc! {"
437 The quˇick brown
438 fox jumps over
439 the lazy dog"})
440 .await;
441 // Test pasting code copied on change
442 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
443 cx.assert_state_matches().await;
444
445 cx.assert_all(indoc! {"
446 The quick brown
447 fox juˇmps over
448 the laˇzy dog"})
449 .await;
450 let mut cx = cx.binding(["shift-v", "j", "c"]);
451 cx.assert(indoc! {"
452 The quˇick brown
453 fox jumps over
454 the lazy dog"})
455 .await;
456 // Test pasting code copied on delete
457 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
458 cx.assert_state_matches().await;
459
460 cx.assert_all(indoc! {"
461 The quick brown
462 fox juˇmps over
463 the laˇzy dog"})
464 .await;
465 }
466
467 #[gpui::test]
468 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
469 let cx = VimTestContext::new(cx, true).await;
470 let mut cx = cx.binding(["v", "w", "y"]);
471 cx.assert("The quick ˇbrown", "The quick ˇbrown");
472 cx.assert_clipboard_content(Some("brown"));
473 let mut cx = cx.binding(["v", "w", "j", "y"]);
474 cx.assert(
475 indoc! {"
476 The ˇquick brown
477 fox jumps over
478 the lazy dog"},
479 indoc! {"
480 The ˇquick brown
481 fox jumps over
482 the lazy dog"},
483 );
484 cx.assert_clipboard_content(Some(indoc! {"
485 quick brown
486 fox jumps o"}));
487 cx.assert(
488 indoc! {"
489 The quick brown
490 fox jumps over
491 the ˇlazy dog"},
492 indoc! {"
493 The quick brown
494 fox jumps over
495 the ˇlazy dog"},
496 );
497 cx.assert_clipboard_content(Some("lazy d"));
498 cx.assert(
499 indoc! {"
500 The quick brown
501 fox jumps ˇover
502 the lazy dog"},
503 indoc! {"
504 The quick brown
505 fox jumps ˇover
506 the lazy dog"},
507 );
508 cx.assert_clipboard_content(Some(indoc! {"
509 over
510 t"}));
511 let mut cx = cx.binding(["v", "b", "k", "y"]);
512 cx.assert(
513 indoc! {"
514 The ˇquick brown
515 fox jumps over
516 the lazy dog"},
517 indoc! {"
518 ˇThe quick brown
519 fox jumps over
520 the lazy dog"},
521 );
522 cx.assert_clipboard_content(Some("The q"));
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 fox jumps over
535 the l"}));
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(indoc! {"
547 quick brown
548 fox jumps o"}));
549 }
550
551 #[gpui::test]
552 async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
553 let mut cx = VimTestContext::new(cx, true).await;
554 cx.set_state(
555 indoc! {"
556 The quick brown
557 fox «jumpˇ»s over
558 the lazy dog"},
559 Mode::Visual { line: false },
560 );
561 cx.simulate_keystroke("y");
562 cx.set_state(
563 indoc! {"
564 The quick brown
565 fox jumpˇs over
566 the lazy dog"},
567 Mode::Normal,
568 );
569 cx.simulate_keystroke("p");
570 cx.assert_state(
571 indoc! {"
572 The quick brown
573 fox jumpsjumpˇs over
574 the lazy dog"},
575 Mode::Normal,
576 );
577
578 cx.set_state(
579 indoc! {"
580 The quick brown
581 fox juˇmps over
582 the lazy dog"},
583 Mode::Visual { line: true },
584 );
585 cx.simulate_keystroke("d");
586 cx.assert_state(
587 indoc! {"
588 The quick brown
589 the laˇzy dog"},
590 Mode::Normal,
591 );
592 cx.set_state(
593 indoc! {"
594 The quick brown
595 the «lazˇ»y dog"},
596 Mode::Visual { line: false },
597 );
598 cx.simulate_keystroke("p");
599 cx.assert_state(
600 indoc! {"
601 The quick brown
602 the
603 ˇfox jumps over
604 dog"},
605 Mode::Normal,
606 );
607 }
608}