diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 582171d8a2..2027943a0f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1e6e5685b9..50a382439a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -572,7 +572,7 @@ pub struct Editor { project: Option>, focused: bool, blink_manager: ModelHandle, - show_local_selections: bool, + pub show_local_selections: bool, mode: EditorMode, replica_id_mapping: Option>, show_gutter: bool, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 16bccb6963..48f502639c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -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)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 310883d1d6..25a5ba5131 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -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, cx: &mut Win fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext) { 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) { 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, ) { 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) { 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) { 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) { 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| { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index bca7af852d..12fd8dbd2b 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -7,7 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim}; pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { 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 { + 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("~"); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index db451cec12..dda8dea1e4 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -28,7 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) { fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { 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); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 7f2b8c4434..a291419413 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -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, 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; + } } diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 23b545abd8..d0dbb9e306 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -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) } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 0b4f19fe13..7359178f0e 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -50,6 +50,26 @@ pub struct EditorState { pub operator_stack: Vec, } +#[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, - pub repeat_actions: Vec, + pub recorded_actions: Vec, + pub recorded_selection: RecordedSelection, } #[derive(Debug)] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 48d34d7094..35abdaf834 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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::(cx), + editor.selections.newest::(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); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index b7ea0811f0..acd55a0954 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -277,7 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { 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; diff --git a/crates/vim/test_data/test_change_case.json b/crates/vim/test_data/test_change_case.json index 1c0cad0b93..10eb93b227 100644 --- a/crates/vim/test_data/test_change_case.json +++ b/crates/vim/test_data/test_change_case.json @@ -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"}} diff --git a/crates/vim/test_data/test_repeat_visual.json b/crates/vim/test_data/test_repeat_visual.json new file mode 100644 index 0000000000..cb83addcfb --- /dev/null +++ b/crates/vim/test_data/test_repeat_visual.json @@ -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"}}