mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
vim: ALlow counts on insert actions
This re-uses the existing repeat infrastructure.
This commit is contained in:
parent
cee549e1ef
commit
d868d00985
@ -533,7 +533,7 @@
|
||||
// TODO: Move this to a dock open action
|
||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||
"cmd-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -499,7 +499,7 @@
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{state::Mode, Vim};
|
||||
use crate::{normal::repeat, state::Mode, Vim};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::{actions, AppContext, ViewContext};
|
||||
use gpui::{actions, Action, AppContext, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(normal_before);
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
let should_repeat = Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count().unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
})
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if should_repeat {
|
||||
repeat::repeat(cx, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::executor::Deterministic;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
@ -40,4 +57,78 @@ mod test {
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state("Tesˇt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_counts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ-hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("h----ˇ-ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("---ˇ-h-----ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----h-----ello--ˇ-\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_repeat(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("--ˇ-hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ--hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("-----ˇ---hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["1", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod paste;
|
||||
mod repeat;
|
||||
pub(crate) mod repeat;
|
||||
mod scroll;
|
||||
mod search;
|
||||
pub mod substitute;
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
insert::NormalBefore,
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use gpui::{actions, Action, AppContext, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
@ -17,6 +18,27 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if super::InsertBefore.id() == action.id()
|
||||
|| super::InsertAfter.id() == action.id()
|
||||
|| super::InsertFirstNonWhitespace.id() == action.id()
|
||||
|| super::InsertEndOfLine.id() == action.id()
|
||||
{
|
||||
Some(super::InsertBefore.boxed_clone())
|
||||
} else if super::InsertLineAbove.id() == action.id()
|
||||
|| super::InsertLineBelow.id() == action.id()
|
||||
{
|
||||
Some(super::InsertLineBelow.boxed_clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
@ -28,127 +50,156 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
});
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.take_count();
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
|
||||
}
|
||||
|
||||
vim.workspace_state.replaying = true;
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.take_count();
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
}
|
||||
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} 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)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
}
|
||||
|
||||
// insert internally uses repeat to handle counts
|
||||
// vim doesn't treat 3a1 as though you literally repeated a1
|
||||
// 3 times, instead it inserts the content thrice at the insert position.
|
||||
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
|
||||
if let Some(ReplayableAction::Action(action)) = actions.last() {
|
||||
if action.id() == NormalBefore.id() {
|
||||
actions.pop();
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_actions = actions.clone();
|
||||
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
|
||||
|
||||
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
|
||||
|
||||
// if we came from insert mode we're just doing repititions 2 onwards.
|
||||
if from_insert_mode {
|
||||
count -= 1;
|
||||
new_actions[0] = actions[0].clone();
|
||||
}
|
||||
|
||||
for _ in 1..count {
|
||||
new_actions.append(actions.clone().as_mut());
|
||||
}
|
||||
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
|
||||
actions = new_actions;
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} 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)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -15,8 +15,8 @@ use anyhow::Result;
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use editor::{movement, Editor, EditorMode, Event};
|
||||
use gpui::{
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
|
||||
AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
@ -284,6 +284,16 @@ impl Vim {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
self.workspace_state.recording = false;
|
||||
self.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
|
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
@ -0,0 +1,36 @@
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"5"}
|
||||
{"Key":"i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"5"}
|
||||
{"Key":"a"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"shift-i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-a"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"i"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"i"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}}
|
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
@ -0,0 +1,23 @@
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"o"}
|
||||
{"Key":"k"}
|
||||
{"Key":"k"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
|
Loading…
Reference in New Issue
Block a user