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