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