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