Detailed changes
@@ -446,12 +446,10 @@
],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
+ "shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
- "shift-i": [
- "vim::SwitchMode",
- "Insert"
- ],
+ "shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"r": [
"vim::PushOperator",
@@ -572,7 +572,7 @@ pub struct Editor {
project: Option<ModelHandle<Project>>,
focused: bool,
blink_manager: ModelHandle<BlinkManager>,
- show_local_selections: bool,
+ pub show_local_selections: bool,
mode: EditorMode,
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
show_gutter: bool,
@@ -65,9 +65,9 @@ struct PreviousWordStart {
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
-struct Up {
+pub(crate) struct Up {
#[serde(default)]
- display_lines: bool,
+ pub(crate) display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -93,9 +93,9 @@ struct EndOfLine {
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
-struct StartOfLine {
+pub struct StartOfLine {
#[serde(default)]
- display_lines: bool,
+ pub(crate) display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -66,21 +66,21 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(vim, Motion::Left, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(vim, Motion::Right, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
let times = vim.pop_number_operator(cx);
change_motion(
vim,
@@ -94,7 +94,7 @@ pub fn init(cx: &mut AppContext) {
});
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(
vim,
@@ -161,7 +161,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -175,7 +175,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
});
}
@@ -186,7 +186,7 @@ fn insert_first_non_whitespace(
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -203,7 +203,7 @@ fn insert_first_non_whitespace(
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -217,7 +217,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
@@ -250,7 +250,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.start_recording();
+ vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
@@ -7,7 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| {
let mut ranges = Vec::new();
@@ -22,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
ranges.push(start..end);
cursor_positions.push(start..start);
}
- Mode::Visual | Mode::VisualBlock => {
+ Mode::Visual => {
ranges.push(selection.start..selection.end);
cursor_positions.push(selection.start..selection.start);
}
+ Mode::VisualBlock => {
+ ranges.push(selection.start..selection.end);
+ if cursor_positions.len() == 0 {
+ cursor_positions.push(selection.start..selection.start);
+ }
+ }
Mode::Insert | Mode::Normal => {
let start = selection.start;
let mut end = start;
@@ -97,6 +103,11 @@ mod test {
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
cx.assert_shared_state("ˇABc\n").await;
+ // works in visual block mode
+ cx.set_shared_state("ˇaa\nbb\ncc").await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
+ cx.assert_shared_state("ˇAa\nBb\ncc").await;
+
// works with multiple cursors (zed only)
cx.set_state("aˇßcdˇe\n", Mode::Normal);
cx.simulate_keystroke("~");
@@ -28,7 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -1,5 +1,7 @@
use crate::{
- state::{Mode, ReplayableAction},
+ motion::Motion,
+ state::{Mode, RecordedSelection, ReplayableAction},
+ visual::visual_motion,
Vim,
};
use gpui::{actions, AppContext};
@@ -11,47 +13,127 @@ pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
Vim::update(cx, |vim, cx| {
vim.workspace_state.replaying = false;
+ vim.update_active_editor(cx, |editor, _| {
+ editor.show_local_selections = true;
+ });
vim.switch_mode(Mode::Normal, false, cx)
});
});
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
- Vim::update(cx, |vim, cx| {
- let actions = vim.workspace_state.repeat_actions.clone();
+ 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;
+ return None;
};
- if let Some(new_count) = vim.pop_number_operator(cx) {
- vim.workspace_state.recorded_count = Some(new_count);
- }
+ let count = vim.pop_number_operator(cx);
+
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) => window
- .dispatch_action(editor.id(), action.as_ref(), &mut cx)
- .ok_or_else(|| anyhow::anyhow!("window was closed")),
- ReplayableAction::Insertion {
- text,
- utf16_range_to_replace,
- } => editor.update(&mut cx, |editor, cx| {
- editor.replay_insert_event(
- &text,
- utf16_range_to_replace.clone(),
- cx,
- )
- }),
- }?
+ 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);
}
- 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 => {}
+ }
+
+ let window = cx.window();
+ cx.app_context()
+ .spawn(move |mut cx| async move {
+ for action in actions {
+ match action {
+ ReplayableAction::Action(action) => window
+ .dispatch_action(editor.id(), action.as_ref(), &mut cx)
+ .ok_or_else(|| anyhow::anyhow!("window was closed")),
+ 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);
});
}
@@ -204,4 +286,128 @@ mod test {
Mode::Normal,
);
}
+
+ #[gpui::test]
+ async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // single-line (3 columns)
+ cx.set_shared_state(indoc! {
+ "ˇthe quick brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "ˇo quick brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "w", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "o quick brown
+ fox ˇops over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["f", "r", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "o quick brown
+ fox ops oveˇothe lazy dog"
+ })
+ .await;
+
+ // visual
+ cx.set_shared_state(indoc! {
+ "the ˇquick brown
+ fox jumps over
+ fox jumps over
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
+ cx.assert_shared_state(indoc! {
+ "the ˇumps over
+ fox jumps over
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "the ˇumps over
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["w", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "the umps ˇumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "the umps umps over
+ the ˇog"
+ })
+ .await;
+
+ // block mode (3 rows)
+ cx.set_shared_state(indoc! {
+ "ˇthe quick brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "ˇothe quick brown
+ ofox jumps over
+ othe lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "othe quick brown
+ ofoxˇo jumps over
+ otheo lazy dog"
+ })
+ .await;
+
+ // line mode
+ cx.set_shared_state(indoc! {
+ "ˇthe quick brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "ˇo
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ "o
+ ˇo
+ the lazy dog"
+ })
+ .await;
+ }
}
@@ -10,6 +10,7 @@ actions!(vim, [Substitute, SubstituteLine]);
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
+ vim.start_recording(cx);
let count = vim.pop_number_operator(cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
@@ -17,6 +18,7 @@ pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
Vim::update(cx, |vim, cx| {
+ vim.start_recording(cx);
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
vim.switch_mode(Mode::VisualLine, false, cx)
}
@@ -50,6 +50,26 @@ pub struct EditorState {
pub operator_stack: Vec<Operator>,
}
+#[derive(Default, Clone, Debug)]
+pub enum RecordedSelection {
+ #[default]
+ None,
+ Visual {
+ rows: u32,
+ cols: u32,
+ },
+ SingleLine {
+ cols: u32,
+ },
+ VisualBlock {
+ rows: u32,
+ cols: u32,
+ },
+ VisualLine {
+ rows: u32,
+ },
+}
+
#[derive(Default, Clone)]
pub struct WorkspaceState {
pub search: SearchState,
@@ -59,7 +79,8 @@ pub struct WorkspaceState {
pub stop_recording_after_next_action: bool,
pub replaying: bool,
pub recorded_count: Option<usize>,
- pub repeat_actions: Vec<ReplayableAction>,
+ pub recorded_actions: Vec<ReplayableAction>,
+ pub recorded_selection: RecordedSelection,
}
#[derive(Debug)]
@@ -18,13 +18,13 @@ use gpui::{
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
-use language::{CursorShape, Selection, SelectionGoal};
+use language::{CursorShape, Point, Selection, SelectionGoal};
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use normal::normal_replace;
use serde::Deserialize;
use settings::{Setting, SettingsStore};
-use state::{EditorState, Mode, Operator, WorkspaceState};
+use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
use std::{ops::Range, sync::Arc};
use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace};
@@ -107,7 +107,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
Vim::update(cx, |vim, _| {
if vim.workspace_state.recording {
vim.workspace_state
- .repeat_actions
+ .recorded_actions
.push(ReplayableAction::Action(handled_by.boxed_clone()));
if vim.workspace_state.stop_recording_after_next_action {
@@ -204,7 +204,7 @@ impl Vim {
Vim::update(cx, |vim, _| {
if vim.workspace_state.recording {
vim.workspace_state
- .repeat_actions
+ .recorded_actions
.push(ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace,
@@ -232,16 +232,51 @@ impl Vim {
// TODO: shift-j?
//
- pub fn start_recording(&mut self) {
+ pub fn start_recording(&mut self, cx: &mut WindowContext) {
if !self.workspace_state.replaying {
self.workspace_state.recording = true;
- self.workspace_state.repeat_actions = Default::default();
+ self.workspace_state.recorded_actions = Default::default();
self.workspace_state.recorded_count =
if let Some(Operator::Number(number)) = self.active_operator() {
Some(number)
} else {
None
+ };
+
+ let selections = self
+ .active_editor
+ .and_then(|editor| editor.upgrade(cx))
+ .map(|editor| {
+ let editor = editor.read(cx);
+ (
+ editor.selections.oldest::<Point>(cx),
+ editor.selections.newest::<Point>(cx),
+ )
+ });
+
+ if let Some((oldest, newest)) = selections {
+ self.workspace_state.recorded_selection = match self.state().mode {
+ Mode::Visual if newest.end.row == newest.start.row => {
+ RecordedSelection::SingleLine {
+ cols: newest.end.column - newest.start.column,
+ }
+ }
+ Mode::Visual => RecordedSelection::Visual {
+ rows: newest.end.row - newest.start.row,
+ cols: newest.end.column,
+ },
+ Mode::VisualLine => RecordedSelection::VisualLine {
+ rows: newest.end.row - newest.start.row,
+ },
+ Mode::VisualBlock => RecordedSelection::VisualBlock {
+ rows: newest.end.row.abs_diff(oldest.start.row),
+ cols: newest.end.column.abs_diff(oldest.start.column),
+ },
+ _ => RecordedSelection::None,
}
+ } else {
+ self.workspace_state.recorded_selection = RecordedSelection::None;
+ }
}
}
@@ -251,8 +286,8 @@ impl Vim {
}
}
- pub fn record_current_action(&mut self) {
- self.start_recording();
+ pub fn record_current_action(&mut self, cx: &mut WindowContext) {
+ self.start_recording(cx);
self.stop_recording();
}
@@ -322,7 +357,7 @@ impl Vim {
operator,
Operator::Change | Operator::Delete | Operator::Replace
) {
- self.start_recording()
+ self.start_recording(cx)
};
self.update_state(|state| state.operator_stack.push(operator));
self.sync_vim_settings(cx);
@@ -277,7 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- vim.record_current_action();
+ vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = editor.selections.line_mode;
@@ -16,3 +16,8 @@
{"Key":"shift-v"}
{"Key":"~"}
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
+{"Put":{"state":"ˇaa\nbb\ncc"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"~"}
+{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
@@ -0,0 +1,51 @@
+{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"s"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"w"}
+{"Key":"."}
+{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}}
+{"Key":"f"}
+{"Key":"r"}
+{"Key":"."}
+{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}}
+{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"w"}
+{"Key":"."}
+{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}}
+{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"j"}
+{"Key":"shift-i"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"4"}
+{"Key":"l"}
+{"Key":"."}
+{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}}
+{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"shift-r"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}}