755 lines · 25.2 KB · Rust
RawDownload
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc}; use crate::{    insert::NormalBefore,    motion::Motion,    state::{Mode, Operator, RecordedSelection, ReplayableAction},    visual::visual_motion,    Vim,};use gpui::{actions, Action, ViewContext, WindowContext};use util::ResultExt;use workspace::Workspace; actions!(vim, [Repeat, EndRepeat, ToggleRecord, ReplayLastRecording]); 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 if crate::replace::ToggleReplace.partial_eq(&**action) {                Some(crate::replace::ToggleReplace.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.dot_replaying = false;            vim.switch_mode(Mode::Normal, false, cx)        });    });     workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));    workspace.register_action(|_: &mut Workspace, _: &ToggleRecord, cx| {        Vim::update(cx, |vim, cx| {            if let Some(char) = vim.workspace_state.recording_register.take() {                vim.workspace_state.last_recorded_register = Some(char)            } else {                vim.push_operator(Operator::RecordRegister, cx);            }        })    });     workspace.register_action(|_: &mut Workspace, _: &ReplayLastRecording, cx| {        let Some(register) = Vim::read(cx).workspace_state.last_recorded_register else {            return;        };        replay_register(register, 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>, cx: &mut WindowContext) {        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();        cx.defer(move |cx| this.next(cx))    }     pub fn stop(self) {        self.0.borrow_mut().actions.clear()    }     pub fn next(self, cx: &mut WindowContext) {        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 {            Vim::update(cx, |vim, _| vim.workspace_state.replayer.take());            return;        };        match action {            ReplayableAction::Action(action) => {                if should_replay(&action) {                    cx.dispatch_action(action.boxed_clone());                    cx.defer(move |cx| observe_action(action.boxed_clone(), cx));                }            }            ReplayableAction::Insertion {                text,                utf16_range_to_replace,            } => {                if let Some(editor) = Vim::read(cx).active_editor.clone() {                    editor                        .update(cx, |editor, cx| {                            editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)                        })                        .log_err();                }            }        }        cx.defer(move |cx| self.next(cx));    }} pub(crate) fn record_register(register: char, cx: &mut WindowContext) {    Vim::update(cx, |vim, cx| {        vim.workspace_state.recording_register = Some(register);        vim.workspace_state.recordings.remove(&register);        vim.workspace_state.ignore_current_insertion = true;        vim.clear_operator(cx)    })} pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) {    Vim::update(cx, |vim, cx| {        let mut count = vim.take_count(cx).unwrap_or(1);        vim.clear_operator(cx);         if register == '@' {            let Some(last) = vim.workspace_state.last_replayed_register else {                return;            };            register = last;        }        let Some(actions) = vim.workspace_state.recordings.get(&register) else {            return;        };         let mut repeated_actions = vec![];        while count > 0 {            repeated_actions.extend(actions.iter().cloned());            count -= 1        }         vim.workspace_state.last_replayed_register = Some(register);         vim.workspace_state            .replayer            .get_or_insert_with(|| Replayer::new())            .replay(repeated_actions, cx);    });} pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {    let Some((mut actions, selection)) = Vim::update(cx, |vim, cx| {        let actions = vim.workspace_state.recorded_actions.clone();        if actions.is_empty() {            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);                }            }        }         if vim.workspace_state.replayer.is_none() {            if let Some(recording_register) = vim.workspace_state.recording_register {                vim.workspace_state                    .recordings                    .entry(recording_register)                    .or_default()                    .push(ReplayableAction::Action(Repeat.boxed_clone()));            }        }         Some((actions, 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;    }     actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));     Vim::update(cx, |vim, cx| {        vim.workspace_state.dot_replaying = true;         vim.workspace_state            .replayer            .get_or_insert_with(|| Replayer::new())            .replay(actions, cx);    })} pub(crate) fn observe_action(action: Box<dyn Action>, cx: &mut WindowContext) {    Vim::update(cx, |vim, _| {        if vim.workspace_state.dot_recording {            vim.workspace_state                .recorded_actions                .push(ReplayableAction::Action(action.boxed_clone()));             if vim.workspace_state.stop_recording_after_next_action {                vim.workspace_state.dot_recording = false;                vim.workspace_state.stop_recording_after_next_action = false;            }        }        if vim.workspace_state.replayer.is_none() {            if let Some(recording_register) = vim.workspace_state.recording_register {                vim.workspace_state                    .recordings                    .entry(recording_register)                    .or_default()                    .push(ReplayableAction::Action(action));            }        }    })} pub(crate) fn observe_insertion(    text: &Arc<str>,    range_to_replace: Option<Range<isize>>,    cx: &mut WindowContext,) {    Vim::update(cx, |vim, _| {        if vim.workspace_state.ignore_current_insertion {            vim.workspace_state.ignore_current_insertion = false;            return;        }        if vim.workspace_state.dot_recording {            vim.workspace_state                .recorded_actions                .push(ReplayableAction::Insertion {                    text: text.clone(),                    utf16_range_to_replace: range_to_replace.clone(),                });            if vim.workspace_state.stop_recording_after_next_action {                vim.workspace_state.dot_recording = false;                vim.workspace_state.stop_recording_after_next_action = false;            }        }        if let Some(recording_register) = vim.workspace_state.recording_register {            vim.workspace_state                .recordings                .entry(recording_register)                .or_default()                .push(ReplayableAction::Insertion {                    text: text.clone(),                    utf16_range_to_replace: range_to_replace,                });        }    });} #[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.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_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, 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

Lines 1–500 of 755.