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