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:
Conrad Irwin 2024-07-03 09:03:39 -06:00 committed by GitHub
parent dceb0827e8
commit 3348c3ab4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 491 additions and 316 deletions

View File

@ -126,10 +126,7 @@
}
}
],
"m": [
"vim::PushOperator",
"Mark"
],
"m": ["vim::PushOperator", "Mark"],
"'": [
"vim::PushOperator",
{
@ -151,14 +148,8 @@
"ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
],
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"],
"v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock",
@ -284,10 +275,7 @@
// z commands
"z t": "editor::ScrollCursorTop",
"z z": "editor::ScrollCursorCenter",
"z .": [
"workspace::SendKeystrokes",
"z z ^"
],
"z .": ["workspace::SendKeystrokes", "z z ^"],
"z b": "editor::ScrollCursorBottom",
"z c": "editor::Fold",
"z o": "editor::UnfoldLines",
@ -305,123 +293,36 @@
}
],
// Count support
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
],
"1": ["vim::Number", 1],
"2": ["vim::Number", 2],
"3": ["vim::Number", 3],
"4": ["vim::Number", 4],
"5": ["vim::Number", 5],
"6": ["vim::Number", 6],
"7": ["vim::Number", 7],
"8": ["vim::Number", 8],
"9": ["vim::Number", 9],
// window related commands (ctrl-w X)
"ctrl-w left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w ctrl-h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w ctrl-l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w ctrl-k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w ctrl-j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-down": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w shift-h": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-l": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-k": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-j": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
@ -443,14 +344,8 @@
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": [
"workspace::NewFileInDirection",
"Up"
],
"ctrl-w ctrl-n": [
"workspace::NewFileInDirection",
"Up"
],
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w d": "editor::GoToDefinitionSplit",
"ctrl-w g d": "editor::GoToDefinitionSplit",
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
@ -472,21 +367,12 @@
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
".": "vim::Repeat",
"c": [
"vim::PushOperator",
"Change"
],
"c": ["vim::PushOperator", "Change"],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"d": ["vim::PushOperator", "Delete"],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": [
"vim::PushOperator",
"Yank"
],
"y": ["vim::PushOperator", "Yank"],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
@ -508,36 +394,18 @@
],
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"r": [
"vim::PushOperator",
"Replace"
],
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": [
"vim::PushOperator",
"Indent"
],
"<": [
"vim::PushOperator",
"Outdent"
],
"g u": [
"vim::PushOperator",
"Lowercase"
],
"g shift-u": [
"vim::PushOperator",
"Uppercase"
],
"g ~": [
"vim::PushOperator",
"OppositeCase"
],
"\"": [
"vim::PushOperator",
"Register"
],
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
"\"": ["vim::PushOperator", "Register"],
"q": "vim::ToggleRecord",
"shift-q": "vim::ReplayLastRecording",
"@": ["vim::PushOperator", "ReplayRegister"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
@ -552,10 +420,7 @@
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": [
"vim::PushOperator",
"Register"
],
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
@ -564,10 +429,7 @@
{
"context": "Editor && VimCount && vim_mode != insert",
"bindings": {
"0": [
"vim::Number",
0
]
"0": ["vim::Number", 0]
}
},
{
@ -618,10 +480,7 @@
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {
"s": [
"vim::PushOperator",
"DeleteSurrounds"
]
"s": ["vim::PushOperator", "DeleteSurrounds"]
}
},
{
@ -743,22 +602,10 @@
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines",
"r": [
"vim::PushOperator",
"Replace"
],
"ctrl-c": [
"vim::SwitchMode",
"Normal"
],
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
],
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"i": [
@ -806,10 +653,7 @@
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-r": [
"vim::PushOperator",
"Register"
]
"ctrl-r": ["vim::PushOperator", "Register"]
}
},
{
@ -828,14 +672,8 @@
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
]
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"]
}
},
{

View File

@ -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);

View File

@ -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()

View File

@ -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(&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, 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");
}
}

View File

@ -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 { .. }

View File

@ -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| {

View File

@ -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| {

View 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"}}

View 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"}}

View 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"}}

View 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"}}

View 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"}}

View File

@ -6,7 +6,7 @@ Zed includes a vim emulation layer known as "vim mode". This document aims to de
Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother.
This means Zed will never be 100% Vim compatible, but should be 100% Vim familiar! We expect that our Vim mode already copes with 90% of your workflow, and we'd like to keep improving it. If you find things that you cant yet do in Vim mode, but which you rely on in your current workflow, please leave feedback in the editor itself (`:feedback`), or [file an issue](https://github.com/zed-industries/zed/issues).
This means Zed will never be 100% Vim compatible, but should be 100% Vim familiar! We expect that our Vim mode already copes with 90% of your workflow, and we'd like to keep improving it. If you find things that you cant yet do in Vim mode, but which you rely on in your current workflow, please [file an issue](https://github.com/zed-industries/zed/issues).
## Zed-specific features
@ -78,6 +78,8 @@ Vim mode uses Zed to define concepts like "brackets" (for the `%` key) and "word
Vim mode emulates visual block mode using Zed's multiple cursor support. This again leads to some differences, but is much more powerful.
Vim's macro support (`q` and `@`) is implemented using Zed's actions. This lets us support recording and replaying of autocompleted code, etc. Unlike Vim, Zed does not re-use the yank registers for recording macros, they are two separate namespaces.
Finally, Vim mode's search and replace functionality is backed by Zed's. This means that the pattern syntax is slightly different, see the section on [Regex differences](#regex-differences) for details.
## Custom key bindings