mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-07 11:28:37 +03:00
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
@ -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"]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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"}}
|
@ -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 can’t 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 can’t 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
|
||||
|
Loading…
Reference in New Issue
Block a user