vim: Add support for temporary normal mode (ctrl-o) within insert mode (#19454)
Support has been added for the ctrl-o command within insert mode. Ctrl-o is used to partially enter normal mode for 1 motion to then return back into insert mode. Release Notes: - vim: Added support for `ctrl-o` in insert mode to enter temporary normal mode --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
254ce74036
commit
b1cd9e4d24
14 changed files with 145 additions and 11 deletions
|
@ -304,7 +304,8 @@
|
||||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
"ctrl-r": ["vim::PushOperator", "Register"],
|
"ctrl-r": ["vim::PushOperator", "Register"],
|
||||||
"insert": "vim::ToggleReplace"
|
"insert": "vim::ToggleReplace",
|
||||||
|
"ctrl-o": "vim::TemporaryNormal"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,10 +3,11 @@ use editor::{scroll::Autoscroll, Bias, Editor};
|
||||||
use gpui::{actions, Action, ViewContext};
|
use gpui::{actions, Action, ViewContext};
|
||||||
use language::SelectionGoal;
|
use language::SelectionGoal;
|
||||||
|
|
||||||
actions!(vim, [NormalBefore]);
|
actions!(vim, [NormalBefore, TemporaryNormal]);
|
||||||
|
|
||||||
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
||||||
Vim::action(editor, cx, Vim::normal_before);
|
Vim::action(editor, cx, Vim::normal_before);
|
||||||
|
Vim::action(editor, cx, Vim::temporary_normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Vim {
|
impl Vim {
|
||||||
|
@ -35,6 +36,11 @@ impl Vim {
|
||||||
|
|
||||||
self.repeat(true, cx)
|
self.repeat(true, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn temporary_normal(&mut self, _: &TemporaryNormal, cx: &mut ViewContext<Self>) {
|
||||||
|
self.switch_mode(Mode::Normal, true, cx);
|
||||||
|
self.temp_mode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -88,12 +88,19 @@ impl Render for ModeIndicator {
|
||||||
return div().into_any();
|
return div().into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let vim_readable = vim.read(cx);
|
||||||
|
let mode = if vim_readable.temp_mode {
|
||||||
|
format!("(insert) {}", vim_readable.mode)
|
||||||
|
} else {
|
||||||
|
vim_readable.mode.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let current_operators_description = self.current_operators_description(vim.clone(), cx);
|
let current_operators_description = self.current_operators_description(vim.clone(), cx);
|
||||||
let pending = self
|
let pending = self
|
||||||
.pending_keys
|
.pending_keys
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(¤t_operators_description);
|
.unwrap_or(¤t_operators_description);
|
||||||
Label::new(format!("{} -- {} --", pending, vim.read(cx).mode))
|
Label::new(format!("{} -- {} --", pending, mode))
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
.line_height_style(LineHeightStyle::UiLabel)
|
.line_height_style(LineHeightStyle::UiLabel)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
|
|
@ -185,6 +185,8 @@ impl Vim {
|
||||||
error!("Unexpected normal mode motion operator: {:?}", operator)
|
error!("Unexpected normal mode motion operator: {:?}", operator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Exit temporary normal mode (if active).
|
||||||
|
self.exit_temporary_normal(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
|
pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -483,6 +485,12 @@ impl Vim {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exit_temporary_normal(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.temp_mode {
|
||||||
|
self.switch_mode(Mode::Insert, true, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
|
@ -176,7 +176,7 @@ impl Vim {
|
||||||
.0;
|
.0;
|
||||||
}
|
}
|
||||||
cursor = movement::indented_line_beginning(map, cursor, true);
|
cursor = movement::indented_line_beginning(map, cursor, true);
|
||||||
} else if !is_multiline {
|
} else if !is_multiline && !vim.temp_mode {
|
||||||
cursor = movement::saturating_left(map, cursor)
|
cursor = movement::saturating_left(map, cursor)
|
||||||
}
|
}
|
||||||
cursors.push(cursor);
|
cursors.push(cursor);
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
use crate::{
|
use crate::{
|
||||||
insert::NormalBefore,
|
insert::NormalBefore,
|
||||||
motion::Motion,
|
motion::Motion,
|
||||||
|
normal::InsertBefore,
|
||||||
state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
|
state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
|
@ -308,6 +309,11 @@ impl Vim {
|
||||||
|
|
||||||
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
|
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
|
||||||
|
|
||||||
|
if self.temp_mode {
|
||||||
|
self.temp_mode = false;
|
||||||
|
actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
|
||||||
|
}
|
||||||
|
|
||||||
let globals = Vim::globals(cx);
|
let globals = Vim::globals(cx);
|
||||||
globals.dot_replaying = true;
|
globals.dot_replaying = true;
|
||||||
let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
|
let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
|
||||||
|
|
|
@ -139,6 +139,11 @@ impl Vim {
|
||||||
options |= SearchOptions::REGEX;
|
options |= SearchOptions::REGEX;
|
||||||
}
|
}
|
||||||
search_bar.set_search_options(options, cx);
|
search_bar.set_search_options(options, cx);
|
||||||
|
let prior_mode = if self.temp_mode {
|
||||||
|
Mode::Insert
|
||||||
|
} else {
|
||||||
|
self.mode
|
||||||
|
};
|
||||||
|
|
||||||
self.search = SearchState {
|
self.search = SearchState {
|
||||||
direction,
|
direction,
|
||||||
|
@ -146,7 +151,7 @@ impl Vim {
|
||||||
initial_query: query,
|
initial_query: query,
|
||||||
prior_selections,
|
prior_selections,
|
||||||
prior_operator: self.operator_stack.last().cloned(),
|
prior_operator: self.operator_stack.last().cloned(),
|
||||||
prior_mode: self.mode,
|
prior_mode,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ impl Vim {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
self.exit_temporary_normal(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
|
pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -65,6 +66,7 @@ impl Vim {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
self.exit_temporary_normal(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yank_selections_content(
|
pub fn yank_selections_content(
|
||||||
|
|
|
@ -153,6 +153,7 @@ pub struct VimGlobals {
|
||||||
pub stop_recording_after_next_action: bool,
|
pub stop_recording_after_next_action: bool,
|
||||||
pub ignore_current_insertion: bool,
|
pub ignore_current_insertion: bool,
|
||||||
pub recorded_count: Option<usize>,
|
pub recorded_count: Option<usize>,
|
||||||
|
pub recording_actions: Vec<ReplayableAction>,
|
||||||
pub recorded_actions: Vec<ReplayableAction>,
|
pub recorded_actions: Vec<ReplayableAction>,
|
||||||
pub recorded_selection: RecordedSelection,
|
pub recorded_selection: RecordedSelection,
|
||||||
|
|
||||||
|
@ -339,11 +340,12 @@ impl VimGlobals {
|
||||||
|
|
||||||
pub fn observe_action(&mut self, action: Box<dyn Action>) {
|
pub fn observe_action(&mut self, action: Box<dyn Action>) {
|
||||||
if self.dot_recording {
|
if self.dot_recording {
|
||||||
self.recorded_actions
|
self.recording_actions
|
||||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||||
|
|
||||||
if self.stop_recording_after_next_action {
|
if self.stop_recording_after_next_action {
|
||||||
self.dot_recording = false;
|
self.dot_recording = false;
|
||||||
|
self.recorded_actions = std::mem::take(&mut self.recording_actions);
|
||||||
self.stop_recording_after_next_action = false;
|
self.stop_recording_after_next_action = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,12 +365,13 @@ impl VimGlobals {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.dot_recording {
|
if self.dot_recording {
|
||||||
self.recorded_actions.push(ReplayableAction::Insertion {
|
self.recording_actions.push(ReplayableAction::Insertion {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
utf16_range_to_replace: range_to_replace.clone(),
|
utf16_range_to_replace: range_to_replace.clone(),
|
||||||
});
|
});
|
||||||
if self.stop_recording_after_next_action {
|
if self.stop_recording_after_next_action {
|
||||||
self.dot_recording = false;
|
self.dot_recording = false;
|
||||||
|
self.recorded_actions = std::mem::take(&mut self.recording_actions);
|
||||||
self.stop_recording_after_next_action = false;
|
self.stop_recording_after_next_action = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1570,3 +1570,36 @@ async fn test_sentence_forwards(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
cx.set_shared_state("helˇlo.\n\n\nworld.").await;
|
cx.set_shared_state("helˇlo.\n\n\nworld.").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_ctrl_o_visual(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("helloˇ world.").await;
|
||||||
|
cx.simulate_shared_keystrokes("i ctrl-o v b r l").await;
|
||||||
|
cx.shared_state().await.assert_eq("ˇllllllworld.");
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-o v f w d").await;
|
||||||
|
cx.shared_state().await.assert_eq("ˇorld.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_ctrl_o_position(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("helˇlo world.").await;
|
||||||
|
cx.simulate_shared_keystrokes("i ctrl-o d i w").await;
|
||||||
|
cx.shared_state().await.assert_eq("ˇ world.");
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-o p").await;
|
||||||
|
cx.shared_state().await.assert_eq(" helloˇworld.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("heˇllo world.").await;
|
||||||
|
cx.simulate_shared_keystrokes("x i ctrl-o .").await;
|
||||||
|
cx.shared_state().await.assert_eq("heˇo world.");
|
||||||
|
cx.simulate_shared_keystrokes("l l escape .").await;
|
||||||
|
cx.shared_state().await.assert_eq("hellˇllo world.");
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ use gpui::{
|
||||||
actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent,
|
actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent,
|
||||||
Render, Subscription, View, ViewContext, WeakView,
|
Render, Subscription, View, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
use insert::NormalBefore;
|
use insert::{NormalBefore, TemporaryNormal};
|
||||||
use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
|
use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
|
||||||
pub use mode_indicator::ModeIndicator;
|
pub use mode_indicator::ModeIndicator;
|
||||||
use motion::Motion;
|
use motion::Motion;
|
||||||
|
@ -38,7 +38,7 @@ use serde::Deserialize;
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
|
||||||
use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
|
use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{mem, ops::Range, sync::Arc};
|
||||||
use surrounds::SurroundsType;
|
use surrounds::SurroundsType;
|
||||||
use ui::{IntoElement, VisualContext};
|
use ui::{IntoElement, VisualContext};
|
||||||
use workspace::{self, Pane, Workspace};
|
use workspace::{self, Pane, Workspace};
|
||||||
|
@ -147,6 +147,8 @@ impl editor::Addon for VimAddon {
|
||||||
pub(crate) struct Vim {
|
pub(crate) struct Vim {
|
||||||
pub(crate) mode: Mode,
|
pub(crate) mode: Mode,
|
||||||
pub last_mode: Mode,
|
pub last_mode: Mode,
|
||||||
|
pub temp_mode: bool,
|
||||||
|
pub exit_temporary_mode: bool,
|
||||||
|
|
||||||
/// pre_count is the number before an operator is specified (3 in 3d2d)
|
/// pre_count is the number before an operator is specified (3 in 3d2d)
|
||||||
pre_count: Option<usize>,
|
pre_count: Option<usize>,
|
||||||
|
@ -197,6 +199,8 @@ impl Vim {
|
||||||
cx.new_view(|cx| Vim {
|
cx.new_view(|cx| Vim {
|
||||||
mode: Mode::Normal,
|
mode: Mode::Normal,
|
||||||
last_mode: Mode::Normal,
|
last_mode: Mode::Normal,
|
||||||
|
temp_mode: false,
|
||||||
|
exit_temporary_mode: false,
|
||||||
pre_count: None,
|
pre_count: None,
|
||||||
post_count: None,
|
post_count: None,
|
||||||
operator_stack: Vec::new(),
|
operator_stack: Vec::new(),
|
||||||
|
@ -353,6 +357,16 @@ impl Vim {
|
||||||
/// Called whenever an keystroke is typed so vim can observe all actions
|
/// Called whenever an keystroke is typed so vim can observe all actions
|
||||||
/// and keystrokes accordingly.
|
/// and keystrokes accordingly.
|
||||||
fn observe_keystrokes(&mut self, keystroke_event: &KeystrokeEvent, cx: &mut ViewContext<Self>) {
|
fn observe_keystrokes(&mut self, keystroke_event: &KeystrokeEvent, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.exit_temporary_mode {
|
||||||
|
self.exit_temporary_mode = false;
|
||||||
|
// Don't switch to insert mode if the action is temporary_normal.
|
||||||
|
if let Some(action) = keystroke_event.action.as_ref() {
|
||||||
|
if action.as_any().downcast_ref::<TemporaryNormal>().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.switch_mode(Mode::Insert, false, cx)
|
||||||
|
}
|
||||||
if let Some(action) = keystroke_event.action.as_ref() {
|
if let Some(action) = keystroke_event.action.as_ref() {
|
||||||
// Keystroke is handled by the vim system, so continue forward
|
// Keystroke is handled by the vim system, so continue forward
|
||||||
if action.name().starts_with("vim::") {
|
if action.name().starts_with("vim::") {
|
||||||
|
@ -438,6 +452,17 @@ impl Vim {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut ViewContext<Self>) {
|
pub fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.temp_mode && mode == Mode::Normal {
|
||||||
|
self.temp_mode = false;
|
||||||
|
self.switch_mode(Mode::Normal, leave_selections, cx);
|
||||||
|
self.switch_mode(Mode::Insert, false, cx);
|
||||||
|
return;
|
||||||
|
} else if self.temp_mode
|
||||||
|
&& !matches!(mode, Mode::Visual | Mode::VisualLine | Mode::VisualBlock)
|
||||||
|
{
|
||||||
|
self.temp_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
let last_mode = self.mode;
|
let last_mode = self.mode;
|
||||||
let prior_mode = self.last_mode;
|
let prior_mode = self.last_mode;
|
||||||
let prior_tx = self.current_tx;
|
let prior_tx = self.current_tx;
|
||||||
|
@ -729,7 +754,7 @@ impl Vim {
|
||||||
Vim::update_globals(cx, |globals, cx| {
|
Vim::update_globals(cx, |globals, cx| {
|
||||||
if !globals.dot_replaying {
|
if !globals.dot_replaying {
|
||||||
globals.dot_recording = true;
|
globals.dot_recording = true;
|
||||||
globals.recorded_actions = Default::default();
|
globals.recording_actions = Default::default();
|
||||||
globals.recorded_count = None;
|
globals.recorded_count = None;
|
||||||
|
|
||||||
let selections = self.editor().map(|editor| {
|
let selections = self.editor().map(|editor| {
|
||||||
|
@ -784,6 +809,7 @@ impl Vim {
|
||||||
if globals.dot_recording {
|
if globals.dot_recording {
|
||||||
globals.stop_recording_after_next_action = true;
|
globals.stop_recording_after_next_action = true;
|
||||||
}
|
}
|
||||||
|
self.exit_temporary_mode = self.temp_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops recording actions immediately rather than waiting until after the
|
/// Stops recording actions immediately rather than waiting until after the
|
||||||
|
@ -798,11 +824,13 @@ impl Vim {
|
||||||
let globals = Vim::globals(cx);
|
let globals = Vim::globals(cx);
|
||||||
if globals.dot_recording {
|
if globals.dot_recording {
|
||||||
globals
|
globals
|
||||||
.recorded_actions
|
.recording_actions
|
||||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||||
|
globals.recorded_actions = mem::take(&mut globals.recording_actions);
|
||||||
globals.dot_recording = false;
|
globals.dot_recording = false;
|
||||||
globals.stop_recording_after_next_action = false;
|
globals.stop_recording_after_next_action = false;
|
||||||
}
|
}
|
||||||
|
self.exit_temporary_mode = self.temp_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicitly record one action (equivalents to start_recording and stop_recording)
|
/// Explicitly record one action (equivalents to start_recording and stop_recording)
|
||||||
|
|
11
crates/vim/test_data/test_ctrl_o_dot.json
Normal file
11
crates/vim/test_data/test_ctrl_o_dot.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{"Put":{"state":"heˇllo world."}}
|
||||||
|
{"Key":"x"}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"ctrl-o"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"heˇo world.","mode":"Insert"}}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"escape"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"hellˇllo world.","mode":"Normal"}}
|
10
crates/vim/test_data/test_ctrl_o_position.json
Normal file
10
crates/vim/test_data/test_ctrl_o_position.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{"Put":{"state":"helˇlo world."}}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"ctrl-o"}
|
||||||
|
{"Key":"d"}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Get":{"state":"ˇ world.","mode":"Insert"}}
|
||||||
|
{"Key":"ctrl-o"}
|
||||||
|
{"Key":"p"}
|
||||||
|
{"Get":{"state":" helloˇworld.","mode":"Insert"}}
|
14
crates/vim/test_data/test_ctrl_o_visual.json
Normal file
14
crates/vim/test_data/test_ctrl_o_visual.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{"Put":{"state":"helloˇ world."}}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"ctrl-o"}
|
||||||
|
{"Key":"v"}
|
||||||
|
{"Key":"b"}
|
||||||
|
{"Key":"r"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Get":{"state":"ˇllllllworld.","mode":"Insert"}}
|
||||||
|
{"Key":"ctrl-o"}
|
||||||
|
{"Key":"v"}
|
||||||
|
{"Key":"f"}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Key":"d"}
|
||||||
|
{"Get":{"state":"ˇorld.","mode":"Insert"}}
|
Loading…
Add table
Add a link
Reference in a new issue