455 lines · 14.1 KB · Rust
RawDownload
use editor::{ToOffset, movement};use gpui::{Action, Context, Window};use schemars::JsonSchema;use serde::Deserialize; use crate::{Vim, state::Mode}; /// Pastes text from the specified register at the cursor position.#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]pub struct HelixPaste {    #[serde(default)]    before: bool,} impl Vim {    pub fn helix_paste(        &mut self,        action: &HelixPaste,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.record_current_action(cx);        self.store_visual_marks(window, cx);        let count = Vim::take_count(cx).unwrap_or(1);        // TODO: vim paste calls take_forced_motion here, but I don't know what that does        // (none of the other helix_ methods call it)         self.update_editor(cx, |vim, editor, cx| {            editor.transact(window, cx, |editor, window, cx| {                editor.set_clip_at_line_ends(false, cx);                 let selected_register = vim.selected_register.take();                 let Some((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| {                    globals.read_register(selected_register, Some(editor), cx)                })                .and_then(|reg| {                    (!reg.text.is_empty())                        .then_some(reg.text)                        .zip(reg.clipboard_selections)                }) else {                    return;                };                 let display_map = editor.display_snapshot(cx);                let current_selections = editor.selections.all_adjusted_display(&display_map);                 // The clipboard can have multiple selections, and there can                // be multiple selections. Helix zips them together, so the first                // clipboard entry gets pasted at the first selection, the second                // entry gets pasted at the second selection, and so on. If there                // are more clipboard selections than selections, the extra ones                // don't get pasted anywhere. If there are more selections than                // clipboard selections, the last clipboard selection gets                // pasted at all remaining selections.                 let mut edits = Vec::new();                let mut new_selections = Vec::new();                let mut start_offset = 0;                 let mut replacement_texts: Vec<String> = Vec::new();                 for ix in 0..current_selections.len() {                    let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) {                        let end_offset = start_offset + clip_sel.len;                        let text = text[start_offset..end_offset].to_string();                        start_offset = end_offset + 1;                        text                    } else if let Some(last_text) = replacement_texts.last() {                        // We have more current selections than clipboard selections: repeat the last one.                        last_text.to_owned()                    } else {                        text.to_string()                    };                    replacement_texts.push(to_insert);                }                 let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));                 for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {                    // Helix doesn't care about the head/tail of the selection.                    // Pasting before means pasting before the whole selection.                    let display_point = if line_mode {                        if action.before {                            movement::line_beginning(&display_map, sel.start, false)                        } else {                            if sel.start == sel.end {                                movement::right(                                    &display_map,                                    movement::line_end(&display_map, sel.end, false),                                )                            } else {                                sel.end                            }                        }                    } else if action.before {                        sel.start                    } else if sel.start == sel.end {                        // Helix and Zed differ in how they understand                        // single-point cursors. In Helix, a single-point cursor                        // is "on top" of some character, and pasting after that                        // cursor means that the pasted content should go after                        // that character. (If the cursor is at the end of a                        // line, the pasted content goes on the next line.)                        movement::right(&display_map, sel.end)                    } else {                        sel.end                    };                    let point = display_point.to_point(&display_map);                    let anchor = if action.before {                        display_map.buffer_snapshot().anchor_after(point)                    } else {                        display_map.buffer_snapshot().anchor_before(point)                    };                    edits.push((point..point, to_insert.repeat(count)));                    new_selections.push((anchor, to_insert.len() * count));                }                 editor.edit(edits, cx);                 editor.change_selections(Default::default(), window, cx, |s| {                    let snapshot = s.buffer().clone();                    s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {                        let offset = anchor.to_offset(&snapshot);                        if action.before {                            offset.saturating_sub(len)..offset                        } else {                            offset..(offset + len)                        }                    }));                })            });        });         self.switch_mode(Mode::HelixNormal, true, window, cx);    }} #[cfg(test)]mod test {    use indoc::indoc;     use crate::{state::Mode, test::VimTestContext};     #[gpui::test]    async fn test_paste(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;        cx.enable_helix();        cx.set_state(            indoc! {"            The «quiˇ»ck brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("y w p");         cx.assert_state(            indoc! {"            The quick «quiˇ»brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         // Pasting before the selection:        cx.set_state(            indoc! {"            The quick brown            fox «jumpsˇ» over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            The quick brown            fox «quiˇ»jumps over            the lazy dog."},            Mode::HelixNormal,        );    }     #[gpui::test]    async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;        cx.enable_helix();        cx.set_state(            indoc! {"            The quiˇck brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("y");         // Pasting before the selection:        cx.set_state(            indoc! {"            The quick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps«cˇ» over            the lazy dog."},            Mode::HelixNormal,        );         // Pasting after the selection:        cx.set_state(            indoc! {"            The quick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps «cˇ»over            the lazy dog."},            Mode::HelixNormal,        );         // Pasting after the selection at the end of a line:        cx.set_state(            indoc! {"            The quick brown            fox jumps overˇ            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps over            «cˇ»the lazy dog."},            Mode::HelixNormal,        );    }     #[gpui::test]    async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;        cx.enable_helix();        // Select two blocks of text.        cx.set_state(            indoc! {"            The «quiˇ»ck brown            fox ju«mpsˇ» over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("y");         // Only one cursor: only the first block gets pasted.        cx.set_state(            indoc! {"            ˇThe quick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            «quiˇ»The quick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         // Two cursors: both get pasted.        cx.set_state(            indoc! {"            ˇThe ˇquick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            «quiˇ»The «mpsˇ»quick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         // Three cursors: the second yanked block is duplicated.        cx.set_state(            indoc! {"            ˇThe ˇquick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            «quiˇ»The «mpsˇ»quick brown            fox jumps«mpsˇ» over            the lazy dog."},            Mode::HelixNormal,        );         // Again with three cursors. All three should be pasted twice.        cx.set_state(            indoc! {"            ˇThe ˇquick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("2 shift-p");        cx.assert_state(            indoc! {"            «quiquiˇ»The «mpsmpsˇ»quick brown            fox jumps«mpsmpsˇ» over            the lazy dog."},            Mode::HelixNormal,        );    }     #[gpui::test]    async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;        cx.enable_helix();        cx.set_state(            indoc! {"            The quick brow«n            ˇ»fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("y shift-p");         cx.assert_state(            indoc! {"            «n            ˇ»The quick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         // In line mode, if we're in the middle of a line then pasting before pastes on        // the line before.        cx.set_state(            indoc! {"            The quick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("shift-p");        cx.assert_state(            indoc! {"            The quick brown            «n            ˇ»fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         // In line mode, if we're in the middle of a line then pasting after pastes on        // the line after.        cx.set_state(            indoc! {"            The quick brown            fox jumpsˇ over            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps over            «n            ˇ»the lazy dog."},            Mode::HelixNormal,        );         // If we're currently at the end of a line, "the line after"        // means right after the cursor.        cx.set_state(            indoc! {"            The quick brown            fox jumps overˇ            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps over            «n            ˇ»the lazy dog."},            Mode::HelixNormal,        );         cx.set_state(            indoc! {"             The quick brown            fox jumps overˇ            the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("x y up up p");        cx.assert_state(            indoc! {"             «fox jumps over            ˇ»The quick brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.set_state(            indoc! {"            «The quick brown            fox jumps over            ˇ»the lazy dog."},            Mode::HelixNormal,        );        cx.simulate_keystrokes("y p p");        cx.assert_state(            indoc! {"            The quick brown            fox jumps over            The quick brown            fox jumps over            «The quick brown            fox jumps over            ˇ»the lazy dog."},            Mode::HelixNormal,        );    }}