vim: Support for q and @ (#13761)
Fixes: #1504 Release Notes: - vim: Support for macros (`q` and `@`) to record and replay (#1506, #4448)
This commit is contained in:
parent
dceb0827e8
commit
3348c3ab4c
13 changed files with 491 additions and 316 deletions
|
@ -23,7 +23,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
|
|||
}
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
if count <= 1 || vim.workspace_state.dot_replaying {
|
||||
create_mark(vim, "^".into(), false, cx);
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.dismiss_menus_and_popups(false, cx);
|
||||
|
|
|
@ -61,10 +61,11 @@ impl ModeIndicator {
|
|||
}
|
||||
|
||||
fn current_operators_description(&self, vim: &Vim) -> String {
|
||||
vim.state()
|
||||
.pre_count
|
||||
.map(|count| format!("{}", count))
|
||||
vim.workspace_state
|
||||
.recording_register
|
||||
.map(|reg| format!("recording @{reg} "))
|
||||
.into_iter()
|
||||
.chain(vim.state().pre_count.map(|count| format!("{}", count)))
|
||||
.chain(vim.state().selected_register.map(|reg| format!("\"{reg}")))
|
||||
.chain(
|
||||
vim.state()
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
insert::NormalBefore,
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
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]);
|
||||
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
|
||||
|
@ -44,24 +47,148 @@ fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
|||
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.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(®ister);
|
||||
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(®ister) 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, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
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 Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.take_count(cx);
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
|
@ -85,7 +212,17 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
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;
|
||||
};
|
||||
|
@ -167,42 +304,75 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
|||
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 {
|
||||
if !matches!(
|
||||
cx.update(|cx| Vim::read(cx).workspace_state.replaying),
|
||||
Ok(true)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
|
||||
|
||||
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()))
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.dot_replaying = true;
|
||||
|
||||
vim.workspace_state
|
||||
.replayer
|
||||
.get_or_insert_with(|| Replayer::new())
|
||||
.replay(actions, cx);
|
||||
})
|
||||
.detach_and_log_err(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)]
|
||||
|
@ -510,4 +680,76 @@ mod test {
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||
|
||||
use crate::normal::repeat::Replayer;
|
||||
use crate::surrounds::SurroundsType;
|
||||
use crate::{motion::Motion, object::Object};
|
||||
use collections::HashMap;
|
||||
|
@ -68,6 +69,8 @@ pub enum Operator {
|
|||
Uppercase,
|
||||
OppositeCase,
|
||||
Register,
|
||||
RecordRegister,
|
||||
ReplayRegister,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
|
@ -155,15 +158,23 @@ impl From<String> for Register {
|
|||
pub struct WorkspaceState {
|
||||
pub last_find: Option<Motion>,
|
||||
|
||||
pub recording: bool,
|
||||
pub dot_recording: bool,
|
||||
pub dot_replaying: bool,
|
||||
|
||||
pub stop_recording_after_next_action: bool,
|
||||
pub replaying: bool,
|
||||
pub ignore_current_insertion: bool,
|
||||
pub recorded_count: Option<usize>,
|
||||
pub recorded_actions: Vec<ReplayableAction>,
|
||||
pub recorded_selection: RecordedSelection,
|
||||
|
||||
pub recording_register: Option<char>,
|
||||
pub last_recorded_register: Option<char>,
|
||||
pub last_replayed_register: Option<char>,
|
||||
pub replayer: Option<Replayer>,
|
||||
|
||||
pub last_yank: Option<SharedString>,
|
||||
pub registers: HashMap<char, Register>,
|
||||
pub recordings: HashMap<char, Vec<ReplayableAction>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -228,6 +239,8 @@ impl EditorState {
|
|||
| Some(Operator::FindBackward { .. })
|
||||
| Some(Operator::Mark)
|
||||
| Some(Operator::Register)
|
||||
| Some(Operator::RecordRegister)
|
||||
| Some(Operator::ReplayRegister)
|
||||
| Some(Operator::Jump { .. })
|
||||
)
|
||||
}
|
||||
|
@ -322,6 +335,8 @@ impl Operator {
|
|||
Operator::Lowercase => "gu",
|
||||
Operator::OppositeCase => "g~",
|
||||
Operator::Register => "\"",
|
||||
Operator::RecordRegister => "q",
|
||||
Operator::ReplayRegister => "@",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,6 +348,8 @@ impl Operator {
|
|||
| Operator::Jump { .. }
|
||||
| Operator::FindBackward { .. }
|
||||
| Operator::Register
|
||||
| Operator::RecordRegister
|
||||
| Operator::ReplayRegister
|
||||
| Operator::Replace
|
||||
| Operator::AddSurrounds { target: Some(_) }
|
||||
| Operator::ChangeSurrounds { .. }
|
||||
|
|
|
@ -31,7 +31,11 @@ use gpui::{
|
|||
use language::{CursorShape, Point, SelectionGoal, TransactionId};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::{mark::create_visual_marks, normal_replace};
|
||||
use normal::{
|
||||
mark::create_visual_marks,
|
||||
normal_replace,
|
||||
repeat::{observe_action, observe_insertion, record_register, replay_register},
|
||||
};
|
||||
use replace::multi_replace;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
@ -170,18 +174,7 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext)
|
|||
.as_ref()
|
||||
.map(|action| action.boxed_clone())
|
||||
{
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
observe_action(action.boxed_clone(), cx);
|
||||
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
if action.name().starts_with("vim::") {
|
||||
|
@ -201,7 +194,9 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext)
|
|||
| Operator::DeleteSurrounds
|
||||
| Operator::Mark
|
||||
| Operator::Jump { .. }
|
||||
| Operator::Register,
|
||||
| Operator::Register
|
||||
| Operator::RecordRegister
|
||||
| Operator::ReplayRegister,
|
||||
) => {}
|
||||
Some(_) => {
|
||||
vim.clear_operator(cx);
|
||||
|
@ -254,12 +249,12 @@ impl Vim {
|
|||
}
|
||||
EditorEvent::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
observe_insertion(text, None, cx)
|
||||
}
|
||||
EditorEvent::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
} => observe_insertion(text, range_to_replace.clone(), cx),
|
||||
EditorEvent::TransactionBegun { transaction_id } => Vim::update(cx, |vim, cx| {
|
||||
vim.transaction_begun(*transaction_id, cx);
|
||||
}),
|
||||
|
@ -288,27 +283,6 @@ impl Vim {
|
|||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn record_insertion(
|
||||
text: &Arc<str>,
|
||||
range_to_replace: Option<Range<isize>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
});
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_active_editor<S>(
|
||||
&mut self,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -333,8 +307,8 @@ impl Vim {
|
|||
/// When doing an action that modifies the buffer, we start recording so that `.`
|
||||
/// will replay the action.
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
if !self.workspace_state.dot_replaying {
|
||||
self.workspace_state.dot_recording = true;
|
||||
self.workspace_state.recorded_actions = Default::default();
|
||||
self.workspace_state.recorded_count = None;
|
||||
|
||||
|
@ -376,15 +350,18 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn stop_replaying(&mut self) {
|
||||
self.workspace_state.replaying = false;
|
||||
pub fn stop_replaying(&mut self, _: &mut WindowContext) {
|
||||
self.workspace_state.dot_replaying = false;
|
||||
if let Some(replayer) = self.workspace_state.replayer.take() {
|
||||
replayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// When finishing an action that modifies the buffer, stop recording.
|
||||
/// as you usually call this within a keystroke handler we also ensure that
|
||||
/// the current action is recorded.
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
if self.workspace_state.dot_recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
@ -394,11 +371,11 @@ impl Vim {
|
|||
///
|
||||
/// This doesn't include the current action.
|
||||
pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
|
||||
if self.workspace_state.recording {
|
||||
if self.workspace_state.dot_recording {
|
||||
self.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
self.workspace_state.recording = false;
|
||||
self.workspace_state.dot_recording = false;
|
||||
self.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +488,7 @@ impl Vim {
|
|||
}
|
||||
|
||||
fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
if self.workspace_state.dot_replaying {
|
||||
return self.workspace_state.recorded_count;
|
||||
}
|
||||
|
||||
|
@ -522,7 +499,7 @@ impl Vim {
|
|||
state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
|
||||
}))
|
||||
};
|
||||
if self.workspace_state.recording {
|
||||
if self.workspace_state.dot_recording {
|
||||
self.workspace_state.recorded_count = count;
|
||||
}
|
||||
self.sync_vim_settings(cx);
|
||||
|
@ -898,6 +875,8 @@ impl Vim {
|
|||
Some(Operator::Mark) => Vim::update(cx, |vim, cx| {
|
||||
normal::mark::create_mark(vim, text, false, cx)
|
||||
}),
|
||||
Some(Operator::RecordRegister) => record_register(text.chars().next().unwrap(), cx),
|
||||
Some(Operator::ReplayRegister) => replay_register(text.chars().next().unwrap(), cx),
|
||||
Some(Operator::Register) => Vim::update(cx, |vim, cx| match vim.state().mode {
|
||||
Mode::Insert => {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
|
|
|
@ -610,7 +610,7 @@ pub fn select_match(
|
|||
});
|
||||
if !match_exists {
|
||||
vim.clear_operator(cx);
|
||||
vim.stop_replaying();
|
||||
vim.stop_replaying(cx);
|
||||
return;
|
||||
}
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
|
|
14
crates/vim/test_data/test_record_replay.json
Normal file
14
crates/vim/test_data/test_record_replay.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{"Put":{"state":"ˇhello world"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"w"}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Key":"j"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"ˇj world","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"l"}
|
||||
{"Key":"@"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"j ˇj","mode":"Normal"}}
|
16
crates/vim/test_data/test_record_replay_count.json
Normal file
16
crates/vim/test_data/test_record_replay_count.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{"Put":{"state":"ˇhello world!!"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"a"}
|
||||
{"Key":"v"}
|
||||
{"Key":"3"}
|
||||
{"Key":"l"}
|
||||
{"Key":"s"}
|
||||
{"Key":"0"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"l"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"0ˇo world!!","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"@"}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"000ˇ!","mode":"Normal"}}
|
17
crates/vim/test_data/test_record_replay_dot.json
Normal file
17
crates/vim/test_data/test_record_replay_dot.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{"Put":{"state":"ˇhello world"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"a"}
|
||||
{"Key":"r"}
|
||||
{"Key":"a"}
|
||||
{"Key":"l"}
|
||||
{"Key":"r"}
|
||||
{"Key":"b"}
|
||||
{"Key":"l"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"abˇllo world","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"abˇblo world","mode":"Normal"}}
|
||||
{"Key":"shift-q"}
|
||||
{"Get":{"state":"ababˇo world","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ababˇb world","mode":"Normal"}}
|
35
crates/vim/test_data/test_record_replay_interleaved.json
Normal file
35
crates/vim/test_data/test_record_replay_interleaved.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{"Put":{"state":"ˇhello world"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"z"}
|
||||
{"Key":"r"}
|
||||
{"Key":"a"}
|
||||
{"Key":"l"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"aˇello world","mode":"Normal"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"b"}
|
||||
{"Key":"@"}
|
||||
{"Key":"z"}
|
||||
{"Key":"@"}
|
||||
{"Key":"z"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"aaaˇlo world","mode":"Normal"}}
|
||||
{"Key":"@"}
|
||||
{"Key":"@"}
|
||||
{"Get":{"state":"aaaaˇo world","mode":"Normal"}}
|
||||
{"Key":"@"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaaaaˇworld","mode":"Normal"}}
|
||||
{"Key":"@"}
|
||||
{"Key":"@"}
|
||||
{"Get":{"state":"aaaaaaaˇorld","mode":"Normal"}}
|
||||
{"Key":"q"}
|
||||
{"Key":"z"}
|
||||
{"Key":"r"}
|
||||
{"Key":"b"}
|
||||
{"Key":"l"}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"aaaaaaabˇrld","mode":"Normal"}}
|
||||
{"Key":"@"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaaaaabbbˇd","mode":"Normal"}}
|
14
crates/vim/test_data/test_record_replay_of_dot.json
Normal file
14
crates/vim/test_data/test_record_replay_of_dot.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{"Put":{"state":"ˇhello world"}}
|
||||
{"Key":"r"}
|
||||
{"Key":"o"}
|
||||
{"Key":"q"}
|
||||
{"Key":"w"}
|
||||
{"Key":"."}
|
||||
{"Key":"q"}
|
||||
{"Get":{"state":"ˇoello world","mode":"Normal"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"ˇello world","mode":"Normal"}}
|
||||
{"Key":"@"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇllo world","mode":"Normal"}}
|
Loading…
Add table
Add a link
Reference in a new issue