Piotr OsiewiczEditor docs (#4097)6cbc49e2 years agoHistory
475 lines · 17.4 KB · Rust
RawDownload
use std::{borrow::Cow, cmp}; use editor::{    display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,};use gpui::{impl_actions, ViewContext};use language::{Bias, SelectionGoal};use serde::Deserialize;use workspace::Workspace; use crate::{state::Mode, utils::copy_selections_content, Vim}; #[derive(Clone, Deserialize, PartialEq)]#[serde(rename_all = "camelCase")]struct Paste {    #[serde(default)]    before: bool,    #[serde(default)]    preserve_clipboard: bool,} impl_actions!(vim, [Paste]); pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {    workspace.register_action(paste);} fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {    Vim::update(cx, |vim, cx| {        vim.record_current_action(cx);        vim.update_active_editor(cx, |editor, cx| {            let text_layout_details = editor.text_layout_details(cx);            editor.transact(cx, |editor, cx| {                editor.set_clip_at_line_ends(false, cx);                 let Some(item) = cx.read_from_clipboard() else {                    return;                };                let clipboard_text = Cow::Borrowed(item.text());                if clipboard_text.is_empty() {                    return;                }                 if !action.preserve_clipboard && vim.state().mode.is_visual() {                    copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);                }                 // if we are copying from multi-cursor (of visual block mode), we want                // to                let clipboard_selections =                    item.metadata::<Vec<ClipboardSelection>>()                        .filter(|clipboard_selections| {                            clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine                        });                 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);                 // unlike zed, if you have a multi-cursor selection from vim block mode,                // pasting it will paste it on subsequent lines, even if you don't yet                // have a cursor there.                let mut selections_to_process = Vec::new();                let mut i = 0;                while i < current_selections.len() {                    selections_to_process                        .push((current_selections[i].start..current_selections[i].end, true));                    i += 1;                }                if let Some(clipboard_selections) = clipboard_selections.as_ref() {                    let left = current_selections                        .iter()                        .map(|selection| cmp::min(selection.start.column(), selection.end.column()))                        .min()                        .unwrap();                    let mut row = current_selections.last().unwrap().end.row() + 1;                    while i < clipboard_selections.len() {                        let cursor =                            display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);                        selections_to_process.push((cursor..cursor, false));                        i += 1;                        row += 1;                    }                }                 let first_selection_indent_column =                    clipboard_selections.as_ref().and_then(|zed_selections| {                        zed_selections                            .first()                            .map(|selection| selection.first_line_indent)                    });                let before = action.before || vim.state().mode == Mode::VisualLine;                 let mut edits = Vec::new();                let mut new_selections = Vec::new();                let mut original_indent_columns = Vec::new();                let mut start_offset = 0;                 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {                    let (mut to_insert, original_indent_column) =                        if let Some(clipboard_selections) = &clipboard_selections {                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {                                let end_offset = start_offset + clipboard_selection.len;                                let text = clipboard_text[start_offset..end_offset].to_string();                                start_offset = end_offset + 1;                                (text, Some(clipboard_selection.first_line_indent))                            } else {                                ("".to_string(), first_selection_indent_column)                            }                        } else {                            (clipboard_text.to_string(), first_selection_indent_column)                        };                    let line_mode = to_insert.ends_with("\n");                    let is_multiline = to_insert.contains("\n");                     if line_mode && !before {                        if selection.is_empty() {                            to_insert =                                "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];                        } else {                            to_insert = "\n".to_owned() + &to_insert;                        }                    } else if !line_mode && vim.state().mode == Mode::VisualLine {                        to_insert = to_insert + "\n";                    }                     let display_range = if !selection.is_empty() {                        selection.start..selection.end                    } else if line_mode {                        let point = if before {                            movement::line_beginning(&display_map, selection.start, false)                        } else {                            movement::line_end(&display_map, selection.start, false)                        };                        point..point                    } else {                        let point = if before {                            selection.start                        } else {                            movement::saturating_right(&display_map, selection.start)                        };                        point..point                    };                     let point_range = display_range.start.to_point(&display_map)                        ..display_range.end.to_point(&display_map);                    let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {                        display_map.buffer_snapshot.anchor_before(point_range.start)                    } else {                        display_map.buffer_snapshot.anchor_after(point_range.end)                    };                     if *preserve {                        new_selections.push((anchor, line_mode, is_multiline));                    }                    edits.push((point_range, to_insert));                    original_indent_columns.extend(original_indent_column);                }                 editor.edit_with_block_indent(edits, original_indent_columns, cx);                 // in line_mode vim will insert the new text on the next (or previous if before) line                // 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).                // otherwise vim will insert the next text at (or before) the current cursor position,                // the cursor will go to the last (or first, if is_multiline) inserted character.                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                    s.replace_cursors_with(|map| {                        let mut cursors = Vec::new();                        for (anchor, line_mode, is_multiline) in &new_selections {                            let mut cursor = anchor.to_display_point(map);                            if *line_mode {                                if !before {                                    cursor = movement::down(                                        map,                                        cursor,                                        SelectionGoal::None,                                        false,                                        &text_layout_details,                                    )                                    .0;                                }                                cursor = movement::indented_line_beginning(map, cursor, true);                            } else if !is_multiline {                                cursor = movement::saturating_left(map, cursor)                            }                            cursors.push(cursor);                            if vim.state().mode == Mode::VisualBlock {                                break;                            }                        }                         cursors                    });                })            });        });        vim.switch_mode(Mode::Normal, true, cx);    });} #[cfg(test)]mod test {    use crate::{        state::Mode,        test::{NeovimBackedTestContext, VimTestContext},    };    use indoc::indoc;     #[gpui::test]    async fn test_paste(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;         // single line        cx.set_shared_state(indoc! {"            The quick brown            fox ˇjumps over            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;        cx.assert_shared_clipboard("jumps o").await;        cx.set_shared_state(indoc! {"            The quick brown            fox jumps oveˇr            the lazy dog"})            .await;        cx.simulate_shared_keystroke("p").await;        cx.assert_shared_state(indoc! {"            The quick brown            fox jumps overjumps ˇo            the lazy dog"})            .await;         cx.set_shared_state(indoc! {"            The quick brown            fox jumps oveˇr            the lazy dog"})            .await;        cx.simulate_shared_keystroke("shift-p").await;        cx.assert_shared_state(indoc! {"            The quick brown            fox jumps ovejumps ˇor            the lazy dog"})            .await;         // line mode        cx.set_shared_state(indoc! {"            The quick brown            fox juˇmps over            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["d", "d"]).await;        cx.assert_shared_clipboard("fox jumps over\n").await;        cx.assert_shared_state(indoc! {"            The quick brown            the laˇzy dog"})            .await;        cx.simulate_shared_keystroke("p").await;        cx.assert_shared_state(indoc! {"            The quick brown            the lazy dog            ˇfox jumps over"})            .await;        cx.simulate_shared_keystrokes(["k", "shift-p"]).await;        cx.assert_shared_state(indoc! {"            The quick brown            ˇfox jumps over            the lazy dog            fox jumps over"})            .await;         // multiline, cursor to first character of pasted text.        cx.set_shared_state(indoc! {"            The quick brown            fox jumps ˇover            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["v", "j", "y"]).await;        cx.assert_shared_clipboard("over\nthe lazy do").await;         cx.simulate_shared_keystroke("p").await;        cx.assert_shared_state(indoc! {"            The quick brown            fox jumps oˇover            the lazy dover            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["u", "shift-p"]).await;        cx.assert_shared_state(indoc! {"            The quick brown            fox jumps ˇover            the lazy doover            the lazy dog"})            .await;    }     #[gpui::test]    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;         // copy in visual mode        cx.set_shared_state(indoc! {"                The quick brown                fox jˇumps over                the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;        cx.assert_shared_state(indoc! {"                The quick brown                fox ˇjumps over                the lazy dog"})            .await;        // paste in visual mode        cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])            .await;        cx.assert_shared_state(indoc! {"                The quick brown                fox jumps jumpˇs                the lazy dog"})            .await;        cx.assert_shared_clipboard("over").await;        // paste in visual line mode        cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])            .await;        cx.assert_shared_state(indoc! {"            ˇover            fox jumps jumps            the lazy dog"})            .await;        cx.assert_shared_clipboard("over").await;        // paste in visual block mode        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])            .await;        cx.assert_shared_state(indoc! {"            oveˇrver            overox jumps jumps            overhe lazy dog"})            .await;         // copy in visual line mode        cx.set_shared_state(indoc! {"                The quick brown                fox juˇmps over                the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;        cx.assert_shared_state(indoc! {"                The quick brown                the laˇzy dog"})            .await;        // paste in visual mode        cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;        cx.assert_shared_state(            &indoc! {"                The quick brown                the_                ˇfox jumps over                _dog"}            .replace("_", " "), // Hack for trailing whitespace        )        .await;        cx.assert_shared_clipboard("lazy").await;        cx.set_shared_state(indoc! {"            The quick brown            fox juˇmps over            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;        cx.assert_shared_state(indoc! {"            The quick brown            the laˇzy dog"})            .await;        // paste in visual line mode        cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;        cx.assert_shared_state(indoc! {"            ˇfox jumps over            the lazy dog"})            .await;        cx.assert_shared_clipboard("The quick brown\n").await;    }     #[gpui::test]    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;        // copy in visual block mode        cx.set_shared_state(indoc! {"            The ˇquick brown            fox jumps over            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])            .await;        cx.assert_shared_clipboard("q\nj\nl").await;        cx.simulate_shared_keystrokes(["p"]).await;        cx.assert_shared_state(indoc! {"            The qˇquick brown            fox jjumps over            the llazy dog"})            .await;        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])            .await;        cx.assert_shared_state(indoc! {"            The ˇq brown            fox jjjumps over            the lllazy dog"})            .await;        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])            .await;         cx.set_shared_state(indoc! {"            The ˇquick brown            fox jumps over            the lazy dog"})            .await;        cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;        cx.assert_shared_clipboard("q\nj").await;        cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])            .await;        cx.assert_shared_state(indoc! {"            The qˇqick brown            fox jjmps over            the lzy dog"})            .await;         cx.simulate_shared_keystrokes(["shift-v", "p"]).await;        cx.assert_shared_state(indoc! {"            ˇq            j            fox jjmps over            the lzy dog"})            .await;    }     #[gpui::test]    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new_typescript(cx).await;         cx.set_state(            indoc! {"            class A {ˇ            }        "},            Mode::Normal,        );        cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);        cx.assert_state(            indoc! {"            class A {                a()ˇ{}            }            "},            Mode::Normal,        );        // cursor goes to the first non-blank character in the line;        cx.simulate_keystrokes(["y", "y", "p"]);        cx.assert_state(            indoc! {"            class A {                a(){}                ˇa(){}            }            "},            Mode::Normal,        );        // indentation is preserved when pasting        cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);        cx.assert_state(            indoc! {"                ˇclass A {                    a(){}                class A {                    a(){}                }                "},            Mode::Normal,        );    }}