1use std::{borrow::Cow, cmp};
2
3use editor::{
4 display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
5 DisplayPoint,
6};
7use gpui::{impl_actions, ViewContext};
8use language::{Bias, SelectionGoal};
9use serde::Deserialize;
10use workspace::Workspace;
11
12use crate::{state::Mode, utils::copy_selections_content, Vim};
13
14#[derive(Clone, Deserialize, PartialEq)]
15#[serde(rename_all = "camelCase")]
16struct Paste {
17 #[serde(default)]
18 before: bool,
19 #[serde(default)]
20 preserve_clipboard: bool,
21}
22
23impl_actions!(vim, [Paste]);
24
25pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
26 workspace.register_action(paste);
27}
28
29fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
30 Vim::update(cx, |vim, cx| {
31 vim.record_current_action(cx);
32 vim.update_active_editor(cx, |editor, cx| {
33 let text_layout_details = editor.text_layout_details(cx);
34 editor.transact(cx, |editor, cx| {
35 editor.set_clip_at_line_ends(false, cx);
36
37 let Some(item) = cx.read_from_clipboard() else {
38 return;
39 };
40 let clipboard_text = Cow::Borrowed(item.text());
41 if clipboard_text.is_empty() {
42 return;
43 }
44
45 if !action.preserve_clipboard && vim.state().mode.is_visual() {
46 copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
47 }
48
49 // if we are copying from multi-cursor (of visual block mode), we want
50 // to
51 let clipboard_selections =
52 item.metadata::<Vec<ClipboardSelection>>()
53 .filter(|clipboard_selections| {
54 clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
55 });
56
57 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
58
59 // unlike zed, if you have a multi-cursor selection from vim block mode,
60 // pasting it will paste it on subsequent lines, even if you don't yet
61 // have a cursor there.
62 let mut selections_to_process = Vec::new();
63 let mut i = 0;
64 while i < current_selections.len() {
65 selections_to_process
66 .push((current_selections[i].start..current_selections[i].end, true));
67 i += 1;
68 }
69 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
70 let left = current_selections
71 .iter()
72 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
73 .min()
74 .unwrap();
75 let mut row = current_selections.last().unwrap().end.row() + 1;
76 while i < clipboard_selections.len() {
77 let cursor =
78 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
79 selections_to_process.push((cursor..cursor, false));
80 i += 1;
81 row += 1;
82 }
83 }
84
85 let first_selection_indent_column =
86 clipboard_selections.as_ref().and_then(|zed_selections| {
87 zed_selections
88 .first()
89 .map(|selection| selection.first_line_indent)
90 });
91 let before = action.before || vim.state().mode == Mode::VisualLine;
92
93 let mut edits = Vec::new();
94 let mut new_selections = Vec::new();
95 let mut original_indent_columns = Vec::new();
96 let mut start_offset = 0;
97
98 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
99 let (mut to_insert, original_indent_column) =
100 if let Some(clipboard_selections) = &clipboard_selections {
101 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
102 let end_offset = start_offset + clipboard_selection.len;
103 let text = clipboard_text[start_offset..end_offset].to_string();
104 start_offset = end_offset + 1;
105 (text, Some(clipboard_selection.first_line_indent))
106 } else {
107 ("".to_string(), first_selection_indent_column)
108 }
109 } else {
110 (clipboard_text.to_string(), first_selection_indent_column)
111 };
112 let line_mode = to_insert.ends_with("\n");
113 let is_multiline = to_insert.contains("\n");
114
115 if line_mode && !before {
116 if selection.is_empty() {
117 to_insert =
118 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
119 } else {
120 to_insert = "\n".to_owned() + &to_insert;
121 }
122 } else if !line_mode && vim.state().mode == Mode::VisualLine {
123 to_insert = to_insert + "\n";
124 }
125
126 let display_range = if !selection.is_empty() {
127 selection.start..selection.end
128 } else if line_mode {
129 let point = if before {
130 movement::line_beginning(&display_map, selection.start, false)
131 } else {
132 movement::line_end(&display_map, selection.start, false)
133 };
134 point..point
135 } else {
136 let point = if before {
137 selection.start
138 } else {
139 movement::saturating_right(&display_map, selection.start)
140 };
141 point..point
142 };
143
144 let point_range = display_range.start.to_point(&display_map)
145 ..display_range.end.to_point(&display_map);
146 let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
147 display_map.buffer_snapshot.anchor_before(point_range.start)
148 } else {
149 display_map.buffer_snapshot.anchor_after(point_range.end)
150 };
151
152 if *preserve {
153 new_selections.push((anchor, line_mode, is_multiline));
154 }
155 edits.push((point_range, to_insert));
156 original_indent_columns.extend(original_indent_column);
157 }
158
159 editor.edit_with_block_indent(edits, original_indent_columns, cx);
160
161 // in line_mode vim will insert the new text on the next (or previous if before) line
162 // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
163 // otherwise vim will insert the next text at (or before) the current cursor position,
164 // the cursor will go to the last (or first, if is_multiline) inserted character.
165 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
166 s.replace_cursors_with(|map| {
167 let mut cursors = Vec::new();
168 for (anchor, line_mode, is_multiline) in &new_selections {
169 let mut cursor = anchor.to_display_point(map);
170 if *line_mode {
171 if !before {
172 cursor = movement::down(
173 map,
174 cursor,
175 SelectionGoal::None,
176 false,
177 &text_layout_details,
178 )
179 .0;
180 }
181 cursor = movement::indented_line_beginning(map, cursor, true);
182 } else if !is_multiline {
183 cursor = movement::saturating_left(map, cursor)
184 }
185 cursors.push(cursor);
186 if vim.state().mode == Mode::VisualBlock {
187 break;
188 }
189 }
190
191 cursors
192 });
193 })
194 });
195 });
196 vim.switch_mode(Mode::Normal, true, cx);
197 });
198}
199
200// #[cfg(test)]
201// mod test {
202// use crate::{
203// state::Mode,
204// test::{NeovimBackedTestContext, VimTestContext},
205// };
206// use indoc::indoc;
207
208// #[gpui::test]
209// async fn test_paste(cx: &mut gpui::TestAppContext) {
210// let mut cx = NeovimBackedTestContext::new(cx).await;
211
212// // single line
213// cx.set_shared_state(indoc! {"
214// The quick brown
215// fox ˇjumps over
216// the lazy dog"})
217// .await;
218// cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
219// cx.assert_shared_clipboard("jumps o").await;
220// cx.set_shared_state(indoc! {"
221// The quick brown
222// fox jumps oveˇr
223// the lazy dog"})
224// .await;
225// cx.simulate_shared_keystroke("p").await;
226// cx.assert_shared_state(indoc! {"
227// The quick brown
228// fox jumps overjumps ˇo
229// the lazy dog"})
230// .await;
231
232// cx.set_shared_state(indoc! {"
233// The quick brown
234// fox jumps oveˇr
235// the lazy dog"})
236// .await;
237// cx.simulate_shared_keystroke("shift-p").await;
238// cx.assert_shared_state(indoc! {"
239// The quick brown
240// fox jumps ovejumps ˇor
241// the lazy dog"})
242// .await;
243
244// // line mode
245// cx.set_shared_state(indoc! {"
246// The quick brown
247// fox juˇmps over
248// the lazy dog"})
249// .await;
250// cx.simulate_shared_keystrokes(["d", "d"]).await;
251// cx.assert_shared_clipboard("fox jumps over\n").await;
252// cx.assert_shared_state(indoc! {"
253// The quick brown
254// the laˇzy dog"})
255// .await;
256// cx.simulate_shared_keystroke("p").await;
257// cx.assert_shared_state(indoc! {"
258// The quick brown
259// the lazy dog
260// ˇfox jumps over"})
261// .await;
262// cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
263// cx.assert_shared_state(indoc! {"
264// The quick brown
265// ˇfox jumps over
266// the lazy dog
267// fox jumps over"})
268// .await;
269
270// // multiline, cursor to first character of pasted text.
271// cx.set_shared_state(indoc! {"
272// The quick brown
273// fox jumps ˇover
274// the lazy dog"})
275// .await;
276// cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
277// cx.assert_shared_clipboard("over\nthe lazy do").await;
278
279// cx.simulate_shared_keystroke("p").await;
280// cx.assert_shared_state(indoc! {"
281// The quick brown
282// fox jumps oˇover
283// the lazy dover
284// the lazy dog"})
285// .await;
286// cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
287// cx.assert_shared_state(indoc! {"
288// The quick brown
289// fox jumps ˇover
290// the lazy doover
291// the lazy dog"})
292// .await;
293// }
294
295// #[gpui::test]
296// async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
297// let mut cx = NeovimBackedTestContext::new(cx).await;
298
299// // copy in visual mode
300// cx.set_shared_state(indoc! {"
301// The quick brown
302// fox jˇumps over
303// the lazy dog"})
304// .await;
305// cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
306// cx.assert_shared_state(indoc! {"
307// The quick brown
308// fox ˇjumps over
309// the lazy dog"})
310// .await;
311// // paste in visual mode
312// cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
313// .await;
314// cx.assert_shared_state(indoc! {"
315// The quick brown
316// fox jumps jumpˇs
317// the lazy dog"})
318// .await;
319// cx.assert_shared_clipboard("over").await;
320// // paste in visual line mode
321// cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
322// .await;
323// cx.assert_shared_state(indoc! {"
324// ˇover
325// fox jumps jumps
326// the lazy dog"})
327// .await;
328// cx.assert_shared_clipboard("over").await;
329// // paste in visual block mode
330// cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
331// .await;
332// cx.assert_shared_state(indoc! {"
333// oveˇrver
334// overox jumps jumps
335// overhe lazy dog"})
336// .await;
337
338// // copy in visual line mode
339// cx.set_shared_state(indoc! {"
340// The quick brown
341// fox juˇmps over
342// the lazy dog"})
343// .await;
344// cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
345// cx.assert_shared_state(indoc! {"
346// The quick brown
347// the laˇzy dog"})
348// .await;
349// // paste in visual mode
350// cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
351// cx.assert_shared_state(
352// &indoc! {"
353// The quick brown
354// the_
355// ˇfox jumps over
356// _dog"}
357// .replace("_", " "), // Hack for trailing whitespace
358// )
359// .await;
360// cx.assert_shared_clipboard("lazy").await;
361// cx.set_shared_state(indoc! {"
362// The quick brown
363// fox juˇmps over
364// the lazy dog"})
365// .await;
366// cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
367// cx.assert_shared_state(indoc! {"
368// The quick brown
369// the laˇzy dog"})
370// .await;
371// // paste in visual line mode
372// cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
373// cx.assert_shared_state(indoc! {"
374// ˇfox jumps over
375// the lazy dog"})
376// .await;
377// cx.assert_shared_clipboard("The quick brown\n").await;
378// }
379
380// #[gpui::test]
381// async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
382// let mut cx = NeovimBackedTestContext::new(cx).await;
383// // copy in visual block mode
384// cx.set_shared_state(indoc! {"
385// The ˇquick brown
386// fox jumps over
387// the lazy dog"})
388// .await;
389// cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
390// .await;
391// cx.assert_shared_clipboard("q\nj\nl").await;
392// cx.simulate_shared_keystrokes(["p"]).await;
393// cx.assert_shared_state(indoc! {"
394// The qˇquick brown
395// fox jjumps over
396// the llazy dog"})
397// .await;
398// cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
399// .await;
400// cx.assert_shared_state(indoc! {"
401// The ˇq brown
402// fox jjjumps over
403// the lllazy dog"})
404// .await;
405// cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
406// .await;
407
408// cx.set_shared_state(indoc! {"
409// The ˇquick brown
410// fox jumps over
411// the lazy dog"})
412// .await;
413// cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
414// cx.assert_shared_clipboard("q\nj").await;
415// cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
416// .await;
417// cx.assert_shared_state(indoc! {"
418// The qˇqick brown
419// fox jjmps over
420// the lzy dog"})
421// .await;
422
423// cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
424// cx.assert_shared_state(indoc! {"
425// ˇq
426// j
427// fox jjmps over
428// the lzy dog"})
429// .await;
430// }
431
432// #[gpui::test]
433// async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
434// let mut cx = VimTestContext::new_typescript(cx).await;
435
436// cx.set_state(
437// indoc! {"
438// class A {ˇ
439// }
440// "},
441// Mode::Normal,
442// );
443// cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
444// cx.assert_state(
445// indoc! {"
446// class A {
447// a()ˇ{}
448// }
449// "},
450// Mode::Normal,
451// );
452// // cursor goes to the first non-blank character in the line;
453// cx.simulate_keystrokes(["y", "y", "p"]);
454// cx.assert_state(
455// indoc! {"
456// class A {
457// a(){}
458// ˇa(){}
459// }
460// "},
461// Mode::Normal,
462// );
463// // indentation is preserved when pasting
464// cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
465// cx.assert_state(
466// indoc! {"
467// ˇclass A {
468// a(){}
469// class A {
470// a(){}
471// }
472// "},
473// Mode::Normal,
474// );
475// }
476// }