paste.rs

  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// }