use std::{cell::RefCell, rc::Rc};

use crate::{
    Vim,
    insert::NormalBefore,
    motion::Motion,
    normal::InsertBefore,
    state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
};
use editor::Editor;
use gpui::{Action, App, Context, Window, actions};
use workspace::Workspace;

actions!(
    vim,
    [
        /// Repeats the last change.
        Repeat,
        /// Ends the repeat recording.
        EndRepeat,
        /// Toggles macro recording.
        ToggleRecord,
        /// Replays the last recorded macro.
        ReplayLastRecording
    ]
);

fn should_replay(action: &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 if crate::replace::ToggleReplace.partial_eq(&**action) {
                Some(crate::replace::ToggleReplace.boxed_clone())
            } else {
                None
            }
        }
        ReplayableAction::Insertion { .. } => None,
    }
}

pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
    Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| {
        Vim::globals(cx).dot_replaying = false;
        vim.switch_mode(Mode::Normal, false, window, cx)
    });

    Vim::action(editor, cx, |vim, _: &Repeat, window, cx| {
        vim.repeat(false, window, cx)
    });

    Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| {
        let globals = Vim::globals(cx);
        if let Some(char) = globals.recording_register.take() {
            globals.last_recorded_register = Some(char)
        } else {
            vim.push_operator(Operator::RecordRegister, window, cx);
        }
    });

    Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| {
        let Some(register) = Vim::globals(cx).last_recorded_register else {
            return;
        };
        vim.replay_register(register, window, cx)
    });
}

pub struct ReplayerState {
    actions: Vec<ReplayableAction>,
    running: bool,
    ix: usize,
}

#[derive(Clone)]
pub struct Replayer(Rc<RefCell<ReplayerState>>);

impl Replayer {
    pub fn new() -> Self {
        Self(Rc::new(RefCell::new(ReplayerState {
            actions: vec![],
            running: false,
            ix: 0,
        })))
    }

    pub fn replay(&mut self, actions: Vec<ReplayableAction>, window: &mut Window, cx: &mut App) {
        let mut lock = self.0.borrow_mut();
        let range = lock.ix..lock.ix;
        lock.actions.splice(range, actions);
        if lock.running {
            return;
        }
        lock.running = true;
        let this = self.clone();
        window.defer(cx, move |window, cx| {
            this.next(window, cx);
            let Some(workspace) = Workspace::for_window(window, cx) else {
                return;
            };
            let Some(editor) = workspace
                .read(cx)
                .active_item(cx)
                .and_then(|item| item.act_as::<Editor>(cx))
            else {
                return;
            };
            editor.update(cx, |editor, cx| {
                editor
                    .buffer()
                    .update(cx, |multi, cx| multi.finalize_last_transaction(cx))
            });
        })
    }

    pub fn stop(self) {
        self.0.borrow_mut().actions.clear()
    }

    pub fn next(self, window: &mut Window, cx: &mut App) {
        let mut lock = self.0.borrow_mut();
        let action = if lock.ix < 10000 {
            lock.actions.get(lock.ix).cloned()
        } else {
            log::error!("Aborting replay after 10000 actions");
            None
        };
        lock.ix += 1;
        drop(lock);
        let Some(action) = action else {
            // The `globals.dot_replaying = false` is a fail-safe to ensure that
            // this value is always reset, in the case that the focus is moved
            // away from the editor, effectively preventing the `EndRepeat`
            // action from being handled.
            let globals = Vim::globals(cx);
            globals.replayer.take();
            globals.dot_replaying = false;
            return;
        };
        match action {
            ReplayableAction::Action(action) => {
                if should_replay(&*action) {
                    window.dispatch_action(action.boxed_clone(), cx);
                    cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
                }
            }
            ReplayableAction::Insertion {
                text,
                utf16_range_to_replace,
            } => {
                let Some(workspace) = Workspace::for_window(window, cx) else {
                    return;
                };
                let Some(editor) = workspace
                    .read(cx)
                    .active_item(cx)
                    .and_then(|item| item.act_as::<Editor>(cx))
                else {
                    return;
                };
                editor.update(cx, |editor, cx| {
                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
                })
            }
        }
        window.defer(cx, move |window, cx| self.next(window, cx));
    }
}

impl Vim {
    pub(crate) fn record_register(
        &mut self,
        register: char,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let globals = Vim::globals(cx);
        globals.recording_register = Some(register);
        globals.recordings.remove(&register);
        globals.ignore_current_insertion = true;
        self.clear_operator(window, cx)
    }

    pub(crate) fn replay_register(
        &mut self,
        mut register: char,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let mut count = Vim::take_count(cx).unwrap_or(1);
        Vim::take_forced_motion(cx);
        self.clear_operator(window, cx);

        let globals = Vim::globals(cx);
        if register == '@' {
            let Some(last) = globals.last_replayed_register else {
                return;
            };
            register = last;
        }
        let Some(actions) = globals.recordings.get(&register) else {
            return;
        };

        let mut repeated_actions = vec![];
        while count > 0 {
            repeated_actions.extend(actions.iter().cloned());
            count -= 1
        }

        globals.last_replayed_register = Some(register);
        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
        replayer.replay(repeated_actions, window, cx);
    }

    pub(crate) fn repeat(
        &mut self,
        from_insert_mode: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if self.active_operator().is_some() {
            Vim::update_globals(cx, |globals, _| {
                globals.recording_actions.clear();
                globals.recording_count = None;
                globals.dot_recording = false;
                globals.stop_recording_after_next_action = false;
            });
            self.clear_operator(window, cx);
            return;
        }

        Vim::take_forced_motion(cx);
        let count = Vim::take_count(cx);

        let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
            let actions = globals.recorded_actions.clone();
            if actions.is_empty() {
                return None;
            }
            if globals.replayer.is_none()
                && let Some(recording_register) = globals.recording_register
            {
                globals
                    .recordings
                    .entry(recording_register)
                    .or_default()
                    .push(ReplayableAction::Action(Repeat.boxed_clone()));
            }

            let mut mode = None;
            let selection = globals.recorded_selection.clone();
            match selection {
                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
                    globals.recorded_count = None;
                    mode = Some(Mode::Visual);
                }
                RecordedSelection::VisualLine { .. } => {
                    globals.recorded_count = None;
                    mode = Some(Mode::VisualLine)
                }
                RecordedSelection::VisualBlock { .. } => {
                    globals.recorded_count = None;
                    mode = Some(Mode::VisualBlock)
                }
                RecordedSelection::None => {
                    if let Some(count) = count {
                        globals.recorded_count = Some(count);
                    }
                }
            }

            Some((actions, selection, mode))
        }) else {
            return;
        };

        // Dot repeat always uses the recorded register, ignoring any "X
        // override, as the register is an inherent part of the recorded action.
        // For numbered registers, Neovim increments on each dot repeat so after
        // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
        // etc..
        let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
        let next_register = recorded_register
            .filter(|c| matches!(c, '1'..='9'))
            .map(|c| ((c as u8 + 1).min(b'9')) as char);

        self.selected_register = next_register.or(recorded_register);
        if let Some(next_register) = next_register {
            Vim::update_globals(cx, |globals, _| {
                globals.recorded_register_for_dot = Some(next_register)
            })
        };

        if mode != Some(self.mode) {
            if let Some(mode) = mode {
                self.switch_mode(mode, false, window, cx)
            }

            match selection {
                RecordedSelection::SingleLine { cols } => {
                    if cols > 1 {
                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
                    }
                }
                RecordedSelection::Visual { rows, cols } => {
                    self.visual_motion(
                        Motion::Down {
                            display_lines: false,
                        },
                        Some(rows as usize),
                        window,
                        cx,
                    );
                    self.visual_motion(
                        Motion::StartOfLine {
                            display_lines: false,
                        },
                        None,
                        window,
                        cx,
                    );
                    if cols > 1 {
                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
                    }
                }
                RecordedSelection::VisualBlock { rows, cols } => {
                    self.visual_motion(
                        Motion::Down {
                            display_lines: false,
                        },
                        Some(rows as usize),
                        window,
                        cx,
                    );
                    if cols > 1 {
                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
                    }
                }
                RecordedSelection::VisualLine { rows } => {
                    self.visual_motion(
                        Motion::Down {
                            display_lines: false,
                        },
                        Some(rows as usize),
                        window,
                        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()
                && NormalBefore.partial_eq(&**action)
            {
                actions.pop();
            }

            let mut new_actions = actions.clone();
            actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());

            let mut count = cx.global::<VimGlobals>().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;
        }

        actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));

        if self.temp_mode {
            self.temp_mode = false;
            actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
        }

        let globals = Vim::globals(cx);
        globals.dot_replaying = true;
        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();

        replayer.replay(actions, window, cx);
    }
}

#[cfg(test)]
mod test {
    use editor::test::editor_lsp_test_context::EditorLspTestContext;
    use futures::StreamExt;
    use indoc::indoc;

    use gpui::EntityInputHandler;

    use crate::{
        VimGlobals,
        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.shared_state().await.assert_eq("hello\nworlˇd");
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");

        // "d"
        cx.simulate_shared_keystrokes("^ d f o").await;
        cx.simulate_shared_keystrokes("g g .").await;
        cx.shared_state().await.assert_eq("ˇ\nworld\nrld");

        // "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.shared_state()
            .await
            .assert_eq("\nworld\nworld\nrld\nˇrld");

        // "~" (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.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
    }

    #[gpui::test]
    async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        // basic paste repeat uses the unnamed register
        cx.set_shared_state("ˇhello\n").await;
        cx.simulate_shared_keystrokes("y y p").await;
        cx.shared_state().await.assert_eq("hello\nˇhello\n");
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");

        // "_ (blackhole) is recorded and replayed, so the pasted text is still
        // the original yanked line.
        cx.set_shared_state(indoc! {"
            ˇone
            two
            three
            four
        "})
            .await;
        cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
        cx.shared_state().await.assert_eq(indoc! {"
            one
            four
            ˇone
        "});

        // the recorded register is replayed, not whatever is in the unnamed register
        cx.set_shared_state(indoc! {"
            ˇone
            two
        "})
            .await;
        cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
            .await;
        cx.shared_state().await.assert_eq(indoc! {"
            one
            two
            two
            ˇtwo
        "});

        // `"X.` ignores the override and always uses the recorded register.
        // Both `dd` calls go into register `a`, so register `b` is empty and
        // `"bp` pastes nothing.
        cx.set_shared_state(indoc! {"
            ˇone
            two
            three
        "})
            .await;
        cx.simulate_shared_keystrokes("\" a d d \" b .").await;
        cx.shared_state().await.assert_eq(indoc! {"
            ˇthree
        "});
        cx.simulate_shared_keystrokes("\" a p \" b p").await;
        cx.shared_state().await.assert_eq(indoc! {"
            three
            ˇtwo
        "});

        // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
        // Since the cycling behavior caps at register 9, the first line to be
        // deleted `1`, is no longer in any of the registers.
        cx.set_shared_state(indoc! {"
            ˇone
            two
            three
            four
            five
            six
            seven
            eight
            nine
            ten
        "})
            .await;
        cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
        cx.shared_state().await.assert_eq(indoc! {"ˇ"});
        cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
            .await;
        cx.shared_state().await.assert_eq(indoc! {"

            ten
            nine
            eight
            seven
            six
            five
            four
            three
            two
            ˇtwo"});

        // unnamed register repeat: dd records None, so . pastes the same
        // deleted text
        cx.set_shared_state(indoc! {"
            ˇone
            two
            three
        "})
            .await;
        cx.simulate_shared_keystrokes("d d p .").await;
        cx.shared_state().await.assert_eq(indoc! {"
            two
            one
            ˇone
            three
        "});

        // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
        // so the next `.` uses `a` and not 3.
        cx.set_shared_state(indoc! {"
            one
            two
            ˇthree
        "})
            .await;
        cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
            .await;
        cx.shared_state().await.assert_eq(indoc! {"
            one
            two
            three
            one
            ˇone
            two
            three
        "});
    }

    // This needs to be a separate test from `test_dot_repeat_registers_paste`
    // as Neovim doesn't have support for using registers in replace operations
    // by default.
    #[gpui::test]
    async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.set_state(
            indoc! {"
            line ˇone
            line two
            line three
        "},
            Mode::Normal,
        );

        // 1. Yank `one` into register `a`
        // 2. Move down and yank `two` into the default register
        // 3. Replace `two` with the contents of register `a`
        cx.simulate_keystrokes("\" a y w j y w \" a g R w");
        cx.assert_state(
            indoc! {"
            line one
            line onˇe
            line three
        "},
            Mode::Normal,
        );

        // 1. Move down to `three`
        // 2. Repeat the replace operation
        cx.simulate_keystrokes("j .");
        cx.assert_state(
            indoc! {"
            line one
            line one
            line onˇe
        "},
            Mode::Normal,
        );

        // Similar test, but this time using numbered registers, as those should
        // automatically increase on successive uses of `.` .
        cx.set_state(
            indoc! {"
            line ˇone
            line two
            line three
            line four
        "},
            Mode::Normal,
        );

        // 1. Yank `one` into register `1`
        // 2. Yank `two` into register `2`
        // 3. Move down and yank `three` into the default register
        // 4. Replace `three` with the contents of register `1`
        // 5. Move down and repeat
        cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
        cx.assert_state(
            indoc! {"
            line one
            line two
            line one
            line twˇo
        "},
            Mode::Normal,
        );
    }

    #[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, window, cx| {
            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
            editor.replace_text_in_range(None, "ä", window, 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.set_request_handler::<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, position),
                            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, position),
                            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_completion_unicode_bug(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! {"
                ĩлˇк
                ĩлк
            "},
            Mode::Normal,
        );

        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
            move |_, params, _| async move {
                let position = params.text_document_position.position;
                let mut to_the_left = position;
                to_the_left.character -= 2;
                Ok(Some(lsp::CompletionResponse::Array(vec![
                    lsp::CompletionItem {
                        label: "oops".to_string(),
                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                            range: lsp::Range::new(to_the_left, position),
                            new_text: "к!".to_string(),
                        })),
                        ..Default::default()
                    },
                ])))
            },
        );
        cx.simulate_keystrokes("i .");
        request.next().await;
        cx.condition(|editor, _| editor.context_menu_visible())
            .await;
        cx.simulate_keystrokes("enter escape");
        cx.assert_state(
            indoc! {"
                ĩкˇ!к
                ĩлк
            "},
            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.shared_state().await.assert_eq(indoc! {
            "ˇo quick brown
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("j w .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "o quick brown
            fox ˇops over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("f r .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "o quick brown
            fox ops oveˇothe lazy dog"
        });

        // 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.shared_state().await.assert_eq(indoc! {
            "the ˇumps over
            fox jumps over
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq(indoc! {
            "the ˇumps over
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("w .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "the umps ˇumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("j .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "the umps umps over
            the ˇog"
        });

        // 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.shared_state().await.assert_eq(indoc! {
            "ˇothe quick brown
            ofox jumps over
            othe lazy dog"
        });
        cx.simulate_shared_keystrokes("j 4 l .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "othe quick brown
            ofoxˇo jumps over
            otheo lazy dog"
        });

        // 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.shared_state().await.assert_eq(indoc! {
            "ˇo
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("j .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "o
            ˇo
            the lazy dog"
        });
    }

    #[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.shared_state().await.assert_eq(indoc! {
            "ˇ brown
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("j .").await;
        cx.shared_state().await.assert_eq(indoc! {
            " brown
            ˇ over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("j 2 .").await;
        cx.shared_state().await.assert_eq(indoc! {
            " brown
             over
            ˇe lazy dog"
        });
    }

    #[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;

        cx.set_shared_state("ˇhello hello hello\n").await;
        cx.simulate_shared_keystrokes("c f o x escape").await;
        cx.shared_state().await.assert_eq("ˇx hello hello\n");
        cx.simulate_shared_keystrokes(": escape").await;
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq("ˇx hello\n");
    }

    #[gpui::test]
    async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
        // be triggered while in Insert mode, ensuring that an action which
        // moves the focus away from the editor, gets recorded.
        cx.update(|_, cx| {
            cx.bind_keys([gpui::KeyBinding::new(
                "ctrl-f",
                search::buffer_search::Deploy::find(),
                None,
            )])
        });

        cx.set_state("ˇhello", Mode::Normal);

        // We're going to enter insert mode, which will start recording, type a
        // character and then immediately use `ctrl-f` to trigger the buffer
        // search. Triggering the buffer search will move focus away from the
        // editor, effectively stopping the recording immediately after
        // `buffer_search::Deploy` is recorded. The first `escape` is used to
        // dismiss the search bar, while the second is used to move from Insert
        // to Normal mode.
        cx.simulate_keystrokes("i x ctrl-f escape escape");
        cx.run_until_parked();

        // Using the `.` key will dispatch the `vim::Repeat` action, repeating
        // the set of recorded actions. This will eventually focus on the search
        // bar, preventing the `EndRepeat` action from being correctly handled.
        cx.simulate_keystrokes(".");
        cx.run_until_parked();

        // After replay finishes, even though the `EndRepeat` action wasn't
        // handled, seeing as the editor lost focus during replay, the
        // `dot_replaying` value should be set back to `false`.
        assert!(
            !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
            "dot_replaying should be false after repeat completes"
        );
    }

    #[gpui::test]
    async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("hellˇo").await;
        cx.simulate_shared_keystrokes("3 a . escape").await;
        cx.shared_state().await.assert_eq("hello..ˇ.");
        cx.simulate_shared_keystrokes("u").await;
        cx.shared_state().await.assert_eq("hellˇo");
    }

    #[gpui::test]
    async fn test_record_replay(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("ˇhello world").await;
        cx.simulate_shared_keystrokes("q w c w j escape q").await;
        cx.shared_state().await.assert_eq("ˇj world");
        cx.simulate_shared_keystrokes("2 l @ w").await;
        cx.shared_state().await.assert_eq("j ˇj");
    }

    #[gpui::test]
    async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("ˇhello world!!").await;
        cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
            .await;
        cx.shared_state().await.assert_eq("0ˇo world!!");
        cx.simulate_shared_keystrokes("2 @ a").await;
        cx.shared_state().await.assert_eq("000ˇ!");
    }

    #[gpui::test]
    async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("ˇhello world").await;
        cx.simulate_shared_keystrokes("q a r a l r b l q").await;
        cx.shared_state().await.assert_eq("abˇllo world");
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq("abˇblo world");
        cx.simulate_shared_keystrokes("shift-q").await;
        cx.shared_state().await.assert_eq("ababˇo world");
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq("ababˇb world");
    }

    #[gpui::test]
    async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("ˇhello world").await;
        cx.simulate_shared_keystrokes("r o q w . q").await;
        cx.shared_state().await.assert_eq("ˇoello world");
        cx.simulate_shared_keystrokes("d l").await;
        cx.shared_state().await.assert_eq("ˇello world");
        cx.simulate_shared_keystrokes("@ w").await;
        cx.shared_state().await.assert_eq("ˇllo world");
    }

    #[gpui::test]
    async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("ˇhello world").await;
        cx.simulate_shared_keystrokes("q z r a l q").await;
        cx.shared_state().await.assert_eq("aˇello world");
        cx.simulate_shared_keystrokes("q b @ z @ z q").await;
        cx.shared_state().await.assert_eq("aaaˇlo world");
        cx.simulate_shared_keystrokes("@ @").await;
        cx.shared_state().await.assert_eq("aaaaˇo world");
        cx.simulate_shared_keystrokes("@ b").await;
        cx.shared_state().await.assert_eq("aaaaaaˇworld");
        cx.simulate_shared_keystrokes("@ @").await;
        cx.shared_state().await.assert_eq("aaaaaaaˇorld");
        cx.simulate_shared_keystrokes("q z r b l q").await;
        cx.shared_state().await.assert_eq("aaaaaaabˇrld");
        cx.simulate_shared_keystrokes("@ b").await;
        cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
    }

    #[gpui::test]
    async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // Check that, when repeat is preceded by something other than a number,
        // the current operator is cleared, in order to prevent infinite loops.
        cx.set_state("ˇhello world", Mode::Normal);
        cx.simulate_keystrokes("d .");
        assert_eq!(cx.active_operator(), None);
    }

    #[gpui::test]
    async fn test_repeat_clear_repeat(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("d d").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇfox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("d . .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇthe lazy dog"
        });
    }

    #[gpui::test]
    async fn test_repeat_clear_count(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("d d").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇfox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("2 d .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇfox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇthe lazy dog"
        });

        cx.set_shared_state(indoc! {
            "ˇthe quick brown
            fox jumps over
            the lazy dog
            the quick brown
            fox jumps over
            the lazy dog"
        })
        .await;
        cx.simulate_shared_keystrokes("2 d d").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇthe lazy dog
            the quick brown
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes("5 d .").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇthe lazy dog
            the quick brown
            fox jumps over
            the lazy dog"
        });
        cx.simulate_shared_keystrokes(".").await;
        cx.shared_state().await.assert_eq(indoc! {
            "ˇfox jumps over
            the lazy dog"
        });
    }
}
