paste.rs

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