509 lines · 16.3 KB · Rust
RawDownload
use crate::{    insert::NormalBefore,    motion::Motion,    state::{Mode, RecordedSelection, ReplayableAction},    visual::visual_motion,    Vim,};use gpui::{actions, Action, ViewContext, WindowContext};use workspace::Workspace; actions!(vim, [Repeat, EndRepeat]); fn should_replay(action: &Box<dyn Action>) -> bool {    // skip so that we don't leave the character palette open    if editor::actions::ShowCharacterPalette.partial_eq(&**action) {        return false;    }    true} fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {    match action {        ReplayableAction::Action(action) => {            if super::InsertBefore.partial_eq(&**action)                || super::InsertAfter.partial_eq(&**action)                || super::InsertFirstNonWhitespace.partial_eq(&**action)                || super::InsertEndOfLine.partial_eq(&**action)            {                Some(super::InsertBefore.boxed_clone())            } else if super::InsertLineAbove.partial_eq(&**action)                || super::InsertLineBelow.partial_eq(&**action)            {                Some(super::InsertLineBelow.boxed_clone())            } else {                None            }        }        ReplayableAction::Insertion { .. } => None,    }} pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {    workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| {        Vim::update(cx, |vim, cx| {            vim.workspace_state.replaying = false;            vim.switch_mode(Mode::Normal, false, cx)        });    });     workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));} pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {    let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {        let actions = vim.workspace_state.recorded_actions.clone();        if actions.is_empty() {            return None;        }         let Some(editor) = vim.active_editor.clone() else {            return None;        };        let count = vim.take_count(cx);         let selection = vim.workspace_state.recorded_selection.clone();        match selection {            RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {                vim.workspace_state.recorded_count = None;                vim.switch_mode(Mode::Visual, false, cx)            }            RecordedSelection::VisualLine { .. } => {                vim.workspace_state.recorded_count = None;                vim.switch_mode(Mode::VisualLine, false, cx)            }            RecordedSelection::VisualBlock { .. } => {                vim.workspace_state.recorded_count = None;                vim.switch_mode(Mode::VisualBlock, false, cx)            }            RecordedSelection::None => {                if let Some(count) = count {                    vim.workspace_state.recorded_count = Some(count);                }            }        }         Some((actions, editor, selection))    }) else {        return;    };     match selection {        RecordedSelection::SingleLine { cols } => {            if cols > 1 {                visual_motion(Motion::Right, Some(cols as usize - 1), cx)            }        }        RecordedSelection::Visual { rows, cols } => {            visual_motion(                Motion::Down {                    display_lines: false,                },                Some(rows as usize),                cx,            );            visual_motion(                Motion::StartOfLine {                    display_lines: false,                },                None,                cx,            );            if cols > 1 {                visual_motion(Motion::Right, Some(cols as usize - 1), cx)            }        }        RecordedSelection::VisualBlock { rows, cols } => {            visual_motion(                Motion::Down {                    display_lines: false,                },                Some(rows as usize),                cx,            );            if cols > 1 {                visual_motion(Motion::Right, Some(cols as usize - 1), cx);            }        }        RecordedSelection::VisualLine { rows } => {            visual_motion(                Motion::Down {                    display_lines: false,                },                Some(rows as usize),                cx,            );        }        RecordedSelection::None => {}    }     // insert internally uses repeat to handle counts    // vim doesn't treat 3a1 as though you literally repeated a1    // 3 times, instead it inserts the content thrice at the insert position.    if let Some(to_repeat) = repeatable_insert(&actions[0]) {        if let Some(ReplayableAction::Action(action)) = actions.last() {            if NormalBefore.partial_eq(&**action) {                actions.pop();            }        }         let mut new_actions = actions.clone();        actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());         let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);         // if we came from insert mode we're just doing repetitions 2 onwards.        if from_insert_mode {            count -= 1;            new_actions[0] = actions[0].clone();        }         for _ in 1..count {            new_actions.append(actions.clone().as_mut());        }        new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));        actions = new_actions;    }     Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);    let window = cx.window_handle();    cx.spawn(move |mut cx| async move {        editor.update(&mut cx, |editor, _| {            editor.show_local_selections = false;        })?;        for action in actions {            match action {                ReplayableAction::Action(action) => {                    if should_replay(&action) {                        window.update(&mut cx, |_, cx| cx.dispatch_action(action))                    } else {                        Ok(())                    }                }                ReplayableAction::Insertion {                    text,                    utf16_range_to_replace,                } => editor.update(&mut cx, |editor, cx| {                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)                }),            }?        }        editor.update(&mut cx, |editor, _| {            editor.show_local_selections = true;        })?;        window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))    })    .detach_and_log_err(cx);} #[cfg(test)]mod test {    use editor::test::editor_lsp_test_context::EditorLspTestContext;    use futures::StreamExt;    use indoc::indoc;     use gpui::ViewInputHandler;     use crate::{        state::Mode,        test::{NeovimBackedTestContext, VimTestContext},    };     #[gpui::test]    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;         // "o"        cx.set_shared_state("ˇhello").await;        cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])            .await;        cx.assert_shared_state("hello\nworlˇd").await;        cx.simulate_shared_keystrokes(["."]).await;        cx.assert_shared_state("hello\nworld\nworlˇd").await;         // "d"        cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;        cx.simulate_shared_keystrokes(["g", "g", "."]).await;        cx.assert_shared_state("ˇ\nworld\nrld").await;         // "p" (note that it pastes the current clipboard)        cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;        cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])            .await;        cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;         // "~" (note that counts apply to the action taken, not . itself)        cx.set_shared_state("ˇthe quick brown fox").await;        cx.simulate_shared_keystrokes(["2", "~", "."]).await;        cx.set_shared_state("THE ˇquick brown fox").await;        cx.simulate_shared_keystrokes(["3", "."]).await;        cx.set_shared_state("THE QUIˇck brown fox").await;        cx.run_until_parked();        cx.simulate_shared_keystrokes(["."]).await;        cx.assert_shared_state("THE QUICK ˇbrown fox").await;    }     #[gpui::test]    async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;         cx.set_state("hˇllo", Mode::Normal);        cx.simulate_keystrokes(["i"]);         // simulate brazilian input for ä.        cx.update_editor(|editor, cx| {            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);            editor.replace_text_in_range(None, "ä", cx);        });        cx.simulate_keystrokes(["escape"]);        cx.assert_state("hˇällo", Mode::Normal);        cx.simulate_keystrokes(["."]);        cx.assert_state("hˇäällo", Mode::Normal);    }     #[gpui::test]    async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {        VimTestContext::init(cx);        let cx = EditorLspTestContext::new_rust(            lsp::ServerCapabilities {                completion_provider: Some(lsp::CompletionOptions {                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),                    resolve_provider: Some(true),                    ..Default::default()                }),                ..Default::default()            },            cx,        )        .await;        let mut cx = VimTestContext::new_with_lsp(cx, true);         cx.set_state(            indoc! {"            onˇe            two            three        "},            Mode::Normal,        );         let mut request =            cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {                let position = params.text_document_position.position;                Ok(Some(lsp::CompletionResponse::Array(vec![                    lsp::CompletionItem {                        label: "first".to_string(),                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                            range: lsp::Range::new(position.clone(), position.clone()),                            new_text: "first".to_string(),                        })),                        ..Default::default()                    },                    lsp::CompletionItem {                        label: "second".to_string(),                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {                            range: lsp::Range::new(position.clone(), position.clone()),                            new_text: "second".to_string(),                        })),                        ..Default::default()                    },                ])))            });        cx.simulate_keystrokes(["a", "."]);        request.next().await;        cx.condition(|editor, _| editor.context_menu_visible())            .await;        cx.simulate_keystrokes(["down", "enter", "!", "escape"]);         cx.assert_state(            indoc! {"                one.secondˇ!                two                three            "},            Mode::Normal,        );        cx.simulate_keystrokes(["j", "."]);        cx.assert_state(            indoc! {"                one.second!                two.secondˇ!                three            "},            Mode::Normal,        );    }     #[gpui::test]    async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;         // single-line (3 columns)        cx.set_shared_state(indoc! {            "ˇthe quick brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])            .await;        cx.assert_shared_state(indoc! {            "ˇo quick brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "w", "."]).await;        cx.assert_shared_state(indoc! {            "o quick brown            fox ˇops over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["f", "r", "."]).await;        cx.assert_shared_state(indoc! {            "o quick brown            fox ops oveˇothe lazy dog"        })        .await;         // visual        cx.set_shared_state(indoc! {            "the ˇquick brown            fox jumps over            fox jumps over            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["v", "j", "x"]).await;        cx.assert_shared_state(indoc! {            "the ˇumps over            fox jumps over            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["."]).await;        cx.assert_shared_state(indoc! {            "the ˇumps over            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["w", "."]).await;        cx.assert_shared_state(indoc! {            "the umps ˇumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "."]).await;        cx.assert_shared_state(indoc! {            "the umps umps over            the ˇog"        })        .await;         // block mode (3 rows)        cx.set_shared_state(indoc! {            "ˇthe quick brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])            .await;        cx.assert_shared_state(indoc! {            "ˇothe quick brown            ofox jumps over            othe lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;        cx.assert_shared_state(indoc! {            "othe quick brown            ofoxˇo jumps over            otheo lazy dog"        })        .await;         // line mode        cx.set_shared_state(indoc! {            "ˇthe quick brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])            .await;        cx.assert_shared_state(indoc! {            "ˇo            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "."]).await;        cx.assert_shared_state(indoc! {            "o            ˇo            the lazy dog"        })        .await;    }     #[gpui::test]    async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await;         cx.set_shared_state(indoc! {            "ˇthe quick brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;        cx.assert_shared_state(indoc! {            "ˇ brown            fox jumps over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "."]).await;        cx.assert_shared_state(indoc! {            " brown            ˇ over            the lazy dog"        })        .await;        cx.simulate_shared_keystrokes(["j", "2", "."]).await;        cx.assert_shared_state(indoc! {            " brown             over            ˇe lazy dog"        })        .await;    }     #[gpui::test]    async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;         cx.set_state("ˇhello\n", Mode::Normal);        cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape"]);        cx.simulate_keystrokes(["escape"]);        cx.assert_state("ˇjhello\n", Mode::Normal);    }     #[gpui::test]    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx).await; 

Lines 1–500 of 509.