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

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