diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 3ec994335e..83875ab44a 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -173,6 +173,7 @@ "context": "Editor && mode == full", "bindings": { "enter": "editor::Newline", + "shift-enter": "editor::Newline", "cmd-shift-enter": "editor::NewlineAbove", "cmd-enter": "editor::NewlineBelow", "alt-z": "editor::ToggleSoftWrap", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 02c09b33af..a93d8aa3ec 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -103,9 +103,18 @@ ], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", + "ctrl-v": "vim::ToggleVisualBlock", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion + "ctrl-f": "vim::PageDown", + "pagedown": "vim::PageDown", + "ctrl-b": "vim::PageUp", + "pageup": "vim::PageUp", + "ctrl-d": "vim::ScrollDown", + "ctrl-u": "vim::ScrollUp", + "ctrl-e": "vim::LineDown", + "ctrl-y": "vim::LineUp", // "g" commands "g g": "vim::StartOfDocument", "g h": "editor::Hover", @@ -293,14 +302,6 @@ "backwards": true } ], - "ctrl-f": "vim::PageDown", - "pagedown": "vim::PageDown", - "ctrl-b": "vim::PageUp", - "pageup": "vim::PageUp", - "ctrl-d": "vim::ScrollDown", - "ctrl-u": "vim::ScrollUp", - "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp", "r": [ "vim::PushOperator", "Replace" @@ -365,7 +366,7 @@ } }, { - "context": "Editor && vim_mode == visual && !VimWaiting", + "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", "bindings": { "u": "editor::Undo", "o": "vim::OtherEnd", @@ -377,6 +378,11 @@ "s": "vim::Substitute", "c": "vim::Substitute", "~": "vim::ChangeCase", + "shift-i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-a": "vim::InsertAfter", "r": [ "vim::PushOperator", "Replace" @@ -394,7 +400,23 @@ "Normal" ], ">": "editor::Indent", - "<": "editor::Outdent" + "<": "editor::Outdent", + "i": [ + "vim::PushOperator", + { + "Object": { + "around": false + } + } + ], + "a": [ + "vim::PushOperator", + { + "Object": { + "around": true + } + } + ], } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 904e77c9f0..45a37db983 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -577,6 +577,7 @@ pub struct Editor { searchable: bool, cursor_shape: CursorShape, collapse_matches: bool, + autoindent_mode: Option, workspace: Option<(WeakViewHandle, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, @@ -1412,6 +1413,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, keymap_context_layers: Default::default(), @@ -1590,6 +1592,14 @@ impl Editor { self.input_enabled = input_enabled; } + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + pub fn set_read_only(&mut self, read_only: bool) { self.read_only = read_only; } @@ -1722,7 +1732,7 @@ impl Editor { } self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx) + buffer.edit(edits, self.autoindent_mode.clone(), cx) }); } @@ -2197,7 +2207,7 @@ impl Editor { drop(snapshot); self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + buffer.edit(edits, this.autoindent_mode.clone(), cx); }); let new_anchor_selections = new_selections.iter().map(|e| &e.0); @@ -3037,7 +3047,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::EachLine), + this.autoindent_mode.clone(), cx, ); }); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index f70436abeb..4eec92c8eb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -61,10 +61,10 @@ pub fn up_by_rows( goal: SelectionGoal, preserve_column_at_start: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let prev_row = start.row().saturating_sub(row_count); @@ -95,10 +95,10 @@ pub fn down_by_rows( goal: SelectionGoal, preserve_column_at_end: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let new_row = start.row() + row_count; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1921bc0738..6a21c898ef 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -1,7 +1,7 @@ use std::{ cell::Ref, cmp, iter, mem, - ops::{Deref, Range, Sub}, + ops::{Deref, DerefMut, Range, Sub}, sync::Arc, }; @@ -53,7 +53,7 @@ impl SelectionsCollection { } } - fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { + pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { self.display_map.update(cx, |map, cx| map.snapshot(cx)) } @@ -250,6 +250,10 @@ impl SelectionsCollection { resolve(self.oldest_anchor(), &self.buffer(cx)) } + pub fn first_anchor(&self) -> Selection { + self.disjoint[0].clone() + } + pub fn first>( &self, cx: &AppContext, @@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> { } impl<'a> MutableSelectionsCollection<'a> { - fn display_map(&mut self) -> DisplaySnapshot { + pub fn display_map(&mut self) -> DisplaySnapshot { self.collection.display_map(self.cx) } @@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> { self.select_anchors(selections) } + pub fn new_selection_id(&mut self) -> usize { + post_inc(&mut self.next_selection_id) + } + pub fn select_display_ranges(&mut self, ranges: T) where T: IntoIterator>, @@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> { } } +impl<'a> DerefMut for MutableSelectionsCollection<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.collection + } +} + // Panics if passed selections are not in order pub fn resolve_multiple<'a, D, I>( selections: I, diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 893f5e8a85..f1b01f460d 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,4 +1,4 @@ -use crate::Vim; +use crate::{Vim, VimEvent}; use editor::{EditorBlurred, EditorFocused, EditorReleased}; use gpui::AppContext; @@ -22,6 +22,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { vim.set_active_editor(editor.clone(), cx); + cx.emit_global(VimEvent::ModeChanged { + mode: vim.state().mode, + }); }); }); } @@ -48,6 +51,7 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) { vim.active_editor = None; } } + vim.editor_states.remove(&editor.id()) }); }); } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 48cae9f4ae..b110c39dc4 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -34,7 +34,7 @@ impl ModeIndicator { if settings::get::(cx).0 { mode_indicator.mode = cx .has_global::() - .then(|| cx.global::().state.mode); + .then(|| cx.global::().state().mode); } else { mode_indicator.mode.take(); } @@ -46,7 +46,7 @@ impl ModeIndicator { .has_global::() .then(|| { let vim = cx.global::(); - vim.enabled.then(|| vim.state.mode) + vim.enabled.then(|| vim.state().mode) }) .flatten(); @@ -80,14 +80,12 @@ impl View for ModeIndicator { let theme = &theme::current(cx).workspace.status_bar; - // we always choose text to be 12 monospace characters - // so that as the mode indicator changes, the rest of the - // UI stays still. let text = match mode { Mode::Normal => "-- NORMAL --", Mode::Insert => "-- INSERT --", - Mode::Visual { line: false } => "-- VISUAL --", - Mode::Visual { line: true } => "VISUAL LINE", + Mode::Visual => "-- VISUAL --", + Mode::VisualLine => "-- VISUAL LINE --", + Mode::VisualBlock => "-- VISUAL BLOCK --", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index acf9d46ad3..29a1ba7df8 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -147,9 +147,9 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx)); let operator = Vim::read(cx).active_operator(); - match Vim::read(cx).state.mode { + match Vim::read(cx).state().mode { Mode::Normal => normal_motion(motion, operator, times, cx), - Mode::Visual { .. } => visual_motion(motion, times, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), Mode::Insert => { // Shouldn't execute a motion in insert mode. Ignoring } @@ -158,7 +158,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { } fn repeat_motion(backwards: bool, cx: &mut WindowContext) { - let find = match Vim::read(cx).state.last_find.clone() { + let find = match Vim::read(cx).workspace_state.last_find.clone() { Some(Motion::FindForward { before, text }) => { if backwards { Motion::FindBackward { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 5ac3e86165..ca26a7a217 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -116,8 +116,8 @@ pub fn normal_motion( pub fn normal_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - match vim.state.operator_stack.pop() { - Some(Operator::Object { around }) => match vim.state.operator_stack.pop() { + match vim.maybe_pop_operator() { + Some(Operator::Object { around }) => match vim.maybe_pop_operator() { Some(Operator::Change) => change_object(vim, object, around, cx), Some(Operator::Delete) => delete_object(vim, object, around, cx), Some(Operator::Yank) => yank_object(vim, object, around, cx), diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index b3e101262d..90967949bb 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -13,15 +13,15 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext(cx) { - match vim.state.mode { - Mode::Visual { line: true } => { + match vim.state().mode { + Mode::VisualLine => { let start = Point::new(selection.start.row, 0); let end = Point::new(selection.end.row, snapshot.line_len(selection.end.row)); ranges.push(start..end); cursor_positions.push(start..start); } - Mode::Visual { line: false } => { + Mode::Visual | Mode::VisualBlock => { ranges.push(selection.start..selection.end); cursor_positions.push(selection.start..selection.start); } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 5f1a68cfe9..4ca0c42909 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -68,10 +68,10 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext find. fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { - Vim::update(cx, |vim, _| vim.state.search = Default::default()); + Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default()); cx.propagate_action(); } @@ -91,8 +91,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { - let state = &mut vim.state.search; + let state = &mut vim.workspace_state.search; let mut count = state.count; + let direction = state.direction; // in the case that the query has changed, the search bar // will have selected the next match already. @@ -101,8 +102,8 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte { count = count.saturating_sub(1) } - search_bar.select_match(state.direction, count, cx); state.count = 1; + search_bar.select_match(direction, count, cx); search_bar.focus_editor(&Default::default(), cx); }); } diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index d2429433fe..1d53c6831c 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,9 +4,9 @@ use language::Point; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { - let line_mode = vim.state.mode == Mode::Visual { line: true }; - vim.switch_mode(Mode::Insert, true, cx); + let line_mode = vim.state().mode == Mode::VisualLine; vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -32,6 +32,7 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { editor.edit(edits, cx); }); }); + vim.switch_mode(Mode::Insert, true, cx); } #[cfg(test)] @@ -52,7 +53,7 @@ mod test { cx.assert_editor_state("xˇbc\n"); // supports a selection - cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false }); + cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual); cx.assert_editor_state("a«bcˇ»\n"); cx.simulate_keystrokes(["s", "x"]); cx.assert_editor_state("axˇ\n"); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 85e6eab692..c203a89f72 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -62,9 +62,9 @@ pub fn init(cx: &mut AppContext) { } fn object(object: Object, cx: &mut WindowContext) { - match Vim::read(cx).state.mode { + match Vim::read(cx).state().mode { Mode::Normal => normal_object(object, cx), - Mode::Visual { .. } => visual_object(object, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), Mode::Insert => { // Shouldn't execute a text object in insert mode. Ignoring } @@ -72,6 +72,47 @@ fn object(object: Object, cx: &mut WindowContext) { } impl Object { + pub fn is_multiline(self) -> bool { + match self { + Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => { + false + } + Object::Sentence + | Object::Parentheses + | Object::AngleBrackets + | Object::CurlyBrackets + | Object::SquareBrackets => true, + } + } + + pub fn always_expands_both_ways(self) -> bool { + match self { + Object::Word { .. } | Object::Sentence => false, + Object::Quotes + | Object::BackQuotes + | Object::DoubleQuotes + | Object::Parentheses + | Object::SquareBrackets + | Object::CurlyBrackets + | Object::AngleBrackets => true, + } + } + + pub fn target_visual_mode(self, current_mode: Mode) -> Mode { + match self { + Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual, + Object::Word { .. } => current_mode, + Object::Sentence + | Object::Quotes + | Object::BackQuotes + | Object::DoubleQuotes + | Object::Parentheses + | Object::SquareBrackets + | Object::CurlyBrackets + | Object::AngleBrackets => Mode::Visual, + } + } + pub fn range( self, map: &DisplaySnapshot, @@ -87,13 +128,27 @@ impl Object { } } Object::Sentence => sentence(map, relative_to, around), - Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''), - Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'), - Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'), - Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'), - Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'), - Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'), - Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'), + Object::Quotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'') + } + Object::BackQuotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') + } + Object::DoubleQuotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') + } + Object::Parentheses => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') + } + Object::SquareBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') + } + Object::CurlyBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}') + } + Object::AngleBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') + } } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 905bd5fd2a..aacd3d26e0 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -9,14 +9,16 @@ use crate::motion::Motion; pub enum Mode { Normal, Insert, - Visual { line: bool }, + Visual, + VisualLine, + VisualBlock, } impl Mode { pub fn is_visual(&self) -> bool { match self { Mode::Normal | Mode::Insert => false, - Mode::Visual { .. } => true, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true, } } } @@ -39,15 +41,20 @@ pub enum Operator { FindBackward { after: bool }, } -#[derive(Default)] -pub struct VimState { +#[derive(Default, Clone)] +pub struct EditorState { pub mode: Mode, + pub last_mode: Mode, pub operator_stack: Vec, - pub search: SearchState, +} +#[derive(Default, Clone)] +pub struct WorkspaceState { + pub search: SearchState, pub last_find: Option, } +#[derive(Clone)] pub struct SearchState { pub direction: Direction, pub count: usize, @@ -64,7 +71,7 @@ impl Default for SearchState { } } -impl VimState { +impl EditorState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { Mode::Normal => { @@ -74,7 +81,7 @@ impl VimState { CursorShape::Underscore } } - Mode::Visual { .. } => CursorShape::Block, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -87,9 +94,13 @@ impl VimState { ) } + pub fn should_autoindent(&self) -> bool { + !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) + } + pub fn clip_at_line_ends(&self) -> bool { match self.mode { - Mode::Insert | Mode::Visual { .. } => false, + Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false, Mode::Normal => true, } } @@ -101,7 +112,7 @@ impl VimState { "vim_mode", match self.mode { Mode::Normal => "normal", - Mode::Visual { .. } => "visual", + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", Mode::Insert => "insert", }, ); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index eb2e6e3a5f..772d7a2033 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -241,7 +241,7 @@ async fn test_status_indicator( deterministic.run_until_parked(); assert_eq!( cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Visual { line: false }) + Some(Mode::Visual) ); // hides if vim mode is disabled diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 023ed880d2..263692b36e 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -116,7 +116,7 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { let mode = if marked_text.contains("»") { - Mode::Visual { line: false } + Mode::Visual } else { Mode::Normal }; @@ -160,7 +160,7 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn neovim_state(&mut self) -> String { generate_marked_text( self.neovim.text().await.as_str(), - &vec![self.neovim_selection().await], + &self.neovim_selections().await[..], true, ) } @@ -169,9 +169,12 @@ impl<'a> NeovimBackedTestContext<'a> { self.neovim.mode().await.unwrap() } - async fn neovim_selection(&mut self) -> Range { - let neovim_selection = self.neovim.selection().await; - neovim_selection.to_offset(&self.buffer_snapshot()) + async fn neovim_selections(&mut self) -> Vec> { + let neovim_selections = self.neovim.selections().await; + neovim_selections + .into_iter() + .map(|selection| selection.to_offset(&self.buffer_snapshot())) + .collect() } pub async fn assert_state_matches(&mut self) { diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index dd9be10723..fc677f032c 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -1,5 +1,8 @@ #[cfg(feature = "neovim")] -use std::ops::{Deref, DerefMut}; +use std::{ + cmp, + ops::{Deref, DerefMut}, +}; use std::{ops::Range, path::PathBuf}; #[cfg(feature = "neovim")] @@ -135,7 +138,7 @@ impl NeovimConnection { #[cfg(feature = "neovim")] pub async fn set_state(&mut self, marked_text: &str) { - let (text, selection) = parse_state(&marked_text); + let (text, selections) = parse_state(&marked_text); let nvim_buffer = self .nvim @@ -167,6 +170,11 @@ impl NeovimConnection { .await .expect("Could not get neovim window"); + if selections.len() != 1 { + panic!("must have one selection"); + } + let selection = &selections[0]; + let cursor = selection.start; nvim_window .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) @@ -224,7 +232,7 @@ impl NeovimConnection { } #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Option, String, Range) { + pub async fn state(&mut self) -> (Option, String, Vec>) { let nvim_buffer = self .nvim .get_current_buf() @@ -261,16 +269,51 @@ impl NeovimConnection { let mode = match nvim_mode_text.as_ref() { "i" => Some(Mode::Insert), "n" => Some(Mode::Normal), - "v" => Some(Mode::Visual { line: false }), - "V" => Some(Mode::Visual { line: true }), + "v" => Some(Mode::Visual), + "V" => Some(Mode::VisualLine), + "\x16" => Some(Mode::VisualBlock), _ => None, }; + let mut selections = Vec::new(); // Vim uses the index of the first and last character in the selection // Zed uses the index of the positions between the characters, so we need // to add one to the end in visual mode. match mode { - Some(Mode::Visual { .. }) => { + Some(Mode::VisualBlock) if selection_row != cursor_row => { + // in zed we fake a block selecrtion by using multiple cursors (one per line) + // this code emulates that. + // to deal with casees where the selection is not perfectly rectangular we extract + // the content of the selection via the "a register to get the shape correctly. + self.nvim.input("\"aygv").await.unwrap(); + let content = self.nvim.command_output("echo getreg('a')").await.unwrap(); + let lines = content.split("\n").collect::>(); + let top = cmp::min(selection_row, cursor_row); + let left = cmp::min(selection_col, cursor_col); + for row in top..=cmp::max(selection_row, cursor_row) { + let content = if row - top >= lines.len() as u32 { + "" + } else { + lines[(row - top) as usize] + }; + let line_len = self + .read_position(format!("echo strlen(getline({}))", row + 1).as_str()) + .await; + + if left > line_len { + continue; + } + + let start = Point::new(row, left); + let end = Point::new(row, left + content.len() as u32); + if cursor_col >= selection_col { + selections.push(start..end) + } else { + selections.push(end..start) + } + } + } + Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => { if selection_col > cursor_col { let selection_line_length = self.read_position("echo strlen(getline(line('v')))").await; @@ -290,38 +333,37 @@ impl NeovimConnection { cursor_row += 1; } } + selections.push( + Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col), + ) } - Some(Mode::Insert) | Some(Mode::Normal) | None => {} + Some(Mode::Insert) | Some(Mode::Normal) | None => selections + .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), } - let (start, end) = ( - Point::new(selection_row, selection_col), - Point::new(cursor_row, cursor_col), - ); - let state = NeovimData::Get { mode, - state: encode_range(&text, start..end), + state: encode_ranges(&text, &selections), }; if self.data.back() != Some(&state) { self.data.push_back(state.clone()); } - (mode, text, start..end) + (mode, text, selections) } #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Option, String, Range) { + pub async fn state(&mut self) -> (Option, String, Vec>) { if let Some(NeovimData::Get { state: text, mode }) = self.data.front() { - let (text, range) = parse_state(text); - (*mode, text, range) + let (text, ranges) = parse_state(text); + (*mode, text, ranges) } else { panic!("operation does not match recorded script. re-record with --features=neovim"); } } - pub async fn selection(&mut self) -> Range { + pub async fn selections(&mut self) -> Vec> { self.state().await.2 } @@ -421,51 +463,62 @@ impl Handler for NvimHandler { } } -fn parse_state(marked_text: &str) -> (String, Range) { +fn parse_state(marked_text: &str) -> (String, Vec>) { let (text, ranges) = util::test::marked_text_ranges(marked_text, true); - let byte_range = ranges[0].clone(); - let mut point_range = Point::zero()..Point::zero(); - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if ix == byte_range.start { - point_range.start = position; - } - if ix == byte_range.end { - point_range.end = position; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - (text, point_range) + let point_ranges = ranges + .into_iter() + .map(|byte_range| { + let mut point_range = Point::zero()..Point::zero(); + let mut ix = 0; + let mut position = Point::zero(); + for c in text.chars().chain(['\0']) { + if ix == byte_range.start { + point_range.start = position; + } + if ix == byte_range.end { + point_range.end = position; + } + let len_utf8 = c.len_utf8(); + ix += len_utf8; + if c == '\n' { + position.row += 1; + position.column = 0; + } else { + position.column += len_utf8 as u32; + } + } + point_range + }) + .collect::>(); + (text, point_ranges) } #[cfg(feature = "neovim")] -fn encode_range(text: &str, range: Range) -> String { - let mut byte_range = 0..0; - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if position == range.start { - byte_range.start = ix; - } - if position == range.end { - byte_range.end = ix; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - util::test::generate_marked_text(text, &[byte_range], true) +fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { + let byte_ranges = point_ranges + .into_iter() + .map(|range| { + let mut byte_range = 0..0; + let mut ix = 0; + let mut position = Point::zero(); + for c in text.chars().chain(['\0']) { + if position == range.start { + byte_range.start = ix; + } + if position == range.end { + byte_range.end = ix; + } + let len_utf8 = c.len_utf8(); + ix += len_utf8; + if c == '\n' { + position.row += 1; + position.column = 0; + } else { + position.column += len_utf8 as u32; + } + } + byte_range + }) + .collect::>(); + util::test::generate_marked_text(text, &byte_ranges[..], true) } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ff8d835edc..56193723d9 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -76,12 +76,12 @@ impl<'a> VimTestContext<'a> { } pub fn mode(&mut self) -> Mode { - self.cx.read(|cx| cx.global::().state.mode) + self.cx.read(|cx| cx.global::().state().mode) } pub fn active_operator(&mut self) -> Option { self.cx - .read(|cx| cx.global::().state.operator_stack.last().copied()) + .read(|cx| cx.global::().state().operator_stack.last().copied()) } pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e8d69d696c..da1c634682 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,21 +12,21 @@ mod utils; mod visual; use anyhow::Result; -use collections::CommandPaletteFilter; +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, }; -use language::CursorShape; +use language::{CursorShape, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::normal_replace; use serde::Deserialize; use settings::{Setting, SettingsStore}; -use state::{Mode, Operator, VimState}; +use state::{EditorState, Mode, Operator, WorkspaceState}; use std::sync::Arc; -use visual::visual_replace; +use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; struct VimModeSetting(bool); @@ -127,7 +127,9 @@ pub struct Vim { active_editor: Option>, editor_subscription: Option, enabled: bool, - state: VimState, + editor_states: HashMap, + workspace_state: WorkspaceState, + default_state: EditorState, } impl Vim { @@ -143,13 +145,13 @@ impl Vim { } fn set_active_editor(&mut self, editor: ViewHandle, cx: &mut WindowContext) { - self.active_editor = Some(editor.downgrade()); + self.active_editor = Some(editor.clone().downgrade()); self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { Event::SelectionsChanged { local: true } => { let editor = editor.read(cx); if editor.leader_replica_id().is_none() { - let newest_empty = editor.selections.newest::(cx).is_empty(); - local_selections_changed(newest_empty, cx); + let newest = editor.selections.newest::(cx); + local_selections_changed(newest, cx); } } Event::InputIgnored { text } => { @@ -163,8 +165,11 @@ impl Vim { let editor_mode = editor.mode(); let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - if editor_mode == EditorMode::Full && !newest_selection_empty { - self.switch_mode(Mode::Visual { line: false }, true, cx); + if editor_mode == EditorMode::Full + && !newest_selection_empty + && self.state().mode == Mode::Normal + { + self.switch_mode(Mode::Visual, true, cx); } } @@ -181,9 +186,14 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { - let last_mode = self.state.mode; - self.state.mode = mode; - self.state.operator_stack.clear(); + let state = self.state(); + let last_mode = state.mode; + let prior_mode = state.last_mode; + self.update_state(|state| { + state.last_mode = last_mode; + state.mode = mode; + state.operator_stack.clear(); + }); cx.emit_global(VimEvent::ModeChanged { mode }); @@ -196,11 +206,33 @@ impl Vim { // Adjust selections self.update_active_editor(cx, |editor, cx| { + if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock + { + visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) + } + editor.change_selections(None, cx, |s| { + // we cheat with visual block mode and use multiple cursors. + // the cost of this cheat is we need to convert back to a single + // cursor whenever vim would. + if last_mode == Mode::VisualBlock + && (mode != Mode::VisualBlock && mode != Mode::Insert) + { + let tail = s.oldest_anchor().tail(); + let head = s.newest_anchor().head(); + s.select_anchor_ranges(vec![tail..head]); + } else if last_mode == Mode::Insert + && prior_mode == Mode::VisualBlock + && mode != Mode::VisualBlock + { + let pos = s.first_anchor().head(); + s.select_anchor_ranges(vec![pos..pos]) + } + s.move_with(|map, selection| { if last_mode.is_visual() && !mode.is_visual() { let mut point = selection.head(); - if !selection.reversed { + if !selection.reversed && !selection.is_empty() { point = movement::left(map, selection.head()); } selection.collapse_to(point, selection.goal) @@ -215,7 +247,7 @@ impl Vim { } fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { - self.state.operator_stack.push(operator); + self.update_state(|state| state.operator_stack.push(operator)); self.sync_vim_settings(cx); } @@ -228,9 +260,13 @@ impl Vim { } } + fn maybe_pop_operator(&mut self) -> Option { + self.update_state(|state| state.operator_stack.pop()) + } + fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator { - let popped_operator = self.state.operator_stack.pop() - .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); + let popped_operator = self.update_state( |state| state.operator_stack.pop() + ) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); self.sync_vim_settings(cx); popped_operator } @@ -244,12 +280,12 @@ impl Vim { } fn clear_operator(&mut self, cx: &mut WindowContext) { - self.state.operator_stack.clear(); + self.update_state(|state| state.operator_stack.clear()); self.sync_vim_settings(cx); } fn active_operator(&self) -> Option { - self.state.operator_stack.last().copied() + self.state().operator_stack.last().copied() } fn active_editor_input_ignored(text: Arc, cx: &mut WindowContext) { @@ -260,17 +296,21 @@ impl Vim { match Vim::read(cx).active_operator() { Some(Operator::FindForward { before }) => { let find = Motion::FindForward { before, text }; - Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + Vim::update(cx, |vim, _| { + vim.workspace_state.last_find = Some(find.clone()) + }); motion::motion(find, cx) } Some(Operator::FindBackward { after }) => { let find = Motion::FindBackward { after, text }; - Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + Vim::update(cx, |vim, _| { + vim.workspace_state.last_find = Some(find.clone()) + }); motion::motion(find, cx) } - Some(Operator::Replace) => match Vim::read(cx).state.mode { + Some(Operator::Replace) => match Vim::read(cx).state().mode { Mode::Normal => normal_replace(text, cx), - Mode::Visual { .. } => visual_replace(text, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, _ => {} @@ -280,7 +320,6 @@ impl Vim { fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { if self.enabled != enabled { self.enabled = enabled; - self.state = Default::default(); cx.update_default_global::(|filter, _| { if self.enabled { @@ -307,8 +346,29 @@ impl Vim { } } + pub fn state(&self) -> &EditorState { + if let Some(active_editor) = self.active_editor.as_ref() { + if let Some(state) = self.editor_states.get(&active_editor.id()) { + return state; + } + } + + &self.default_state + } + + pub fn update_state(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T { + let mut state = self.state().clone(); + let ret = func(&mut state); + + if let Some(active_editor) = self.active_editor.as_ref() { + self.editor_states.insert(active_editor.id(), state); + } + + ret + } + fn sync_vim_settings(&self, cx: &mut WindowContext) { - let state = &self.state; + let state = self.state(); let cursor_shape = state.cursor_shape(); self.update_active_editor(cx, |editor, cx| { @@ -317,7 +377,8 @@ impl Vim { editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); - editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); + editor.set_autoindent(state.should_autoindent()); + editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); } else { @@ -333,6 +394,7 @@ impl Vim { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); editor.set_input_enabled(true); + editor.set_autoindent(true); editor.selections.line_mode = false; // we set the VimEnabled context on all editors so that we @@ -365,10 +427,14 @@ impl Setting for VimModeSetting { } } -fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { +fn local_selections_changed(newest: Selection, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { - vim.switch_mode(Mode::Visual { line: false }, false, cx) + if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() { + if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) { + vim.switch_mode(Mode::VisualBlock, false, cx); + } else { + vim.switch_mode(Mode::Visual, false, cx) + } } }) } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1716e2d1a5..df7c8cfa45 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,11 +1,14 @@ -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, cmp, sync::Arc}; use collections::HashMap; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection, + display_map::{DisplaySnapshot, ToDisplayPoint}, + movement, + scroll::autoscroll::Autoscroll, + Bias, ClipboardSelection, DisplayPoint, Editor, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; -use language::{AutoindentMode, SelectionGoal}; +use language::{AutoindentMode, Selection, SelectionGoal}; use workspace::Workspace; use crate::{ @@ -21,6 +24,7 @@ actions!( [ ToggleVisual, ToggleVisualLine, + ToggleVisualBlock, VisualDelete, VisualYank, VisualPaste, @@ -29,8 +33,17 @@ actions!( ); pub fn init(cx: &mut AppContext) { - cx.add_action(toggle_visual); - cx.add_action(toggle_visual_line); + cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext| { + toggle_mode(Mode::Visual, cx) + }); + cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext| { + toggle_mode(Mode::VisualLine, cx) + }); + cx.add_action( + |_, _: &ToggleVisualBlock, cx: &mut ViewContext| { + toggle_mode(Mode::VisualBlock, cx) + }, + ); cx.add_action(other_end); cx.add_action(delete); cx.add_action(yank); @@ -40,59 +53,156 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let was_reversed = selection.reversed; + if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { + let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); + visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { + motion.move_point(map, point, goal, times) + }) + } else { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let was_reversed = selection.reversed; + let mut current_head = selection.head(); - let mut current_head = selection.head(); + // our motions assume the current character is after the cursor, + // but in (forward) visual mode the current character is just + // before the end of the selection. - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. + // If the file ends with a newline (which is common) we don't do this. + // so that if you go to the end of such a file you can use "up" to go + // to the previous line and have it work somewhat as expected. + if !selection.reversed + && !selection.is_empty() + && !(selection.end.column() == 0 && selection.end == map.max_point()) + { + current_head = movement::left(map, selection.end) + } - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let Some((new_head, goal)) = + let Some((new_head, goal)) = motion.move_point(map, current_head, selection.goal, times) else { return }; - selection.set_head(new_head, goal); + selection.set_head(new_head, goal); - // ensure the current character is included in the selection. - if !selection.reversed { - // TODO: maybe try clipping left for multi-buffers - let next_point = movement::right(map, selection.end); + // ensure the current character is included in the selection. + if !selection.reversed { + let next_point = if vim.state().mode == Mode::VisualBlock { + movement::saturating_right(map, selection.end) + } else { + movement::right(map, selection.end) + }; - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = movement::right(map, selection.end) + if !(next_point.column() == 0 && next_point == map.max_point()) { + selection.end = next_point; + } } - } - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } + // vim always ensures the anchor character stays selected. + // if our selection has reversed, we need to move the opposite end + // to ensure the anchor is still selected. + if was_reversed && !selection.reversed { + selection.start = movement::left(map, selection.start); + } else if !was_reversed && selection.reversed { + selection.end = movement::right(map, selection.end); + } + }) }); - }); + } }); }); } +pub fn visual_block_motion( + preserve_goal: bool, + editor: &mut Editor, + cx: &mut ViewContext, + mut move_selection: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> Option<(DisplayPoint, SelectionGoal)>, +) { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let map = &s.display_map(); + let mut head = s.newest_anchor().head().to_display_point(map); + let mut tail = s.oldest_anchor().tail().to_display_point(map); + let mut goal = s.newest_anchor().goal; + + let was_reversed = tail.column() > head.column(); + + if !was_reversed && !preserve_goal { + head = movement::saturating_left(map, head); + } + + let Some((new_head, _)) = move_selection(&map, head, goal) else { + return + }; + head = new_head; + + let is_reversed = tail.column() > head.column(); + if was_reversed && !is_reversed { + tail = movement::left(map, tail) + } else if !was_reversed && is_reversed { + tail = movement::right(map, tail) + } + if !is_reversed && !preserve_goal { + head = movement::saturating_right(map, head) + } + + let (start, end) = match goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), + }; + goal = SelectionGoal::ColumnRange { start, end }; + + let columns = if is_reversed { + head.column()..tail.column() + } else if head.column() == tail.column() { + head.column()..(head.column() + 1) + } else { + tail.column()..head.column() + }; + + let mut selections = Vec::new(); + let mut row = tail.row(); + + loop { + let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left); + let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left); + if columns.start <= map.line_len(row) { + let selection = Selection { + id: s.new_selection_id(), + start: start.to_point(map), + end: end.to_point(map), + reversed: is_reversed, + goal: goal.clone(), + }; + + selections.push(selection); + } + if row == head.row() { + break; + } + if tail.row() > head.row() { + row -= 1 + } else { + row += 1 + } + } + + s.select(selections); + }) +} + pub fn visual_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if let Some(Operator::Object { around }) = vim.active_operator() { vim.pop_operator(cx); + let current_mode = vim.state().mode; + let target_mode = object.target_visual_mode(current_mode); + if target_mode != current_mode { + vim.switch_mode(target_mode, true, cx); + } vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { @@ -108,20 +218,21 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { if let Some(range) = object.range(map, head, around) { if !range.is_empty() { - let expand_both_ways = if selection.is_empty() { - true - // contains only one character - } else if let Some((_, start)) = - map.reverse_chars_at(selection.end).next() - { - selection.start == start - } else { - false - }; + let expand_both_ways = + if object.always_expands_both_ways() || selection.is_empty() { + true + // contains only one character + } else if let Some((_, start)) = + map.reverse_chars_at(selection.end).next() + { + selection.start == start + } else { + false + }; if expand_both_ways { - selection.start = range.start; - selection.end = range.end; + selection.start = cmp::min(selection.start, range.start); + selection.end = cmp::max(selection.end, range.end); } else if selection.reversed { selection.start = range.start; } else { @@ -136,28 +247,12 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { }); } -pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::Visual { line: true } => { - vim.switch_mode(Mode::Visual { line: false }, false, cx); - } - Mode::Visual { line: false } => { - vim.switch_mode(Mode::Normal, false, cx); - } - }) -} - -pub fn toggle_visual_line( - _: &mut Workspace, - _: &ToggleVisualLine, - cx: &mut ViewContext, -) { - Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::Visual { line: false } => { - vim.switch_mode(Mode::Visual { line: true }, false, cx); - } - Mode::Visual { line: true } => { +fn toggle_mode(mode: Mode, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + if vim.state().mode == mode { vim.switch_mode(Mode::Normal, false, cx); + } else { + vim.switch_mode(mode, false, cx); } }) } @@ -180,34 +275,39 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext = Default::default(); let line_mode = editor.selections.line_mode; - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if line_mode { + let mut position = selection.head(); + if !selection.reversed { + position = movement::left(map, position); + } + original_columns.insert(selection.id, position.to_point(map).column); } - original_columns.insert(selection.id, position.to_point(map).column); - } - selection.goal = SelectionGoal::None; + selection.goal = SelectionGoal::None; + }); }); - }); - copy_selections_content(editor, line_mode, cx); - editor.insert("", cx); + copy_selections_content(editor, line_mode, cx); + editor.insert("", cx); - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head().to_point(map); - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column + if let Some(column) = original_columns.get(&selection.id) { + cursor.column = *column + } + let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); + if vim.state().mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) }); - }); + }) }); vim.switch_mode(Mode::Normal, true, cx); }); @@ -222,6 +322,9 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) s.move_with(|_, selection| { selection.collapse_to(selection.start, SelectionGoal::None) }); + if vim.state().mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) + } }); }); vim.switch_mode(Mode::Normal, true, cx); @@ -701,7 +804,7 @@ mod test { The quick brown fox «jumpsˇ» over the lazy dog"}, - Mode::Visual { line: false }, + Mode::Visual, ); cx.simulate_keystroke("y"); cx.set_state( @@ -725,7 +828,7 @@ mod test { The quick brown fox ju«mˇ»ps over the lazy dog"}, - Mode::Visual { line: true }, + Mode::VisualLine, ); cx.simulate_keystroke("d"); cx.assert_state( @@ -738,7 +841,7 @@ mod test { indoc! {" The quick brown the «lazyˇ» dog"}, - Mode::Visual { line: false }, + Mode::Visual, ); cx.simulate_keystroke("p"); cx.assert_state( @@ -751,4 +854,218 @@ mod test { Mode::Normal, ); } + + #[gpui::test] + async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v"]).await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["2", "down"]).await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + fox «jˇ»umps over + the «lˇ»azy dog" + }) + .await; + cx.simulate_shared_keystrokes(["e"]).await; + cx.assert_shared_state(indoc! { + "The «quicˇ»k brown + fox «jumpˇ»s over + the «lazyˇ» dog" + }) + .await; + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { + "«ˇThe q»uick brown + «ˇfox j»umps over + «ˇthe l»azy dog" + }) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { + "The «quick brownˇ» + fox «jumps overˇ» + the «lazy dogˇ»" + }) + .await; + cx.simulate_shared_keystrokes(["shift-f", " "]).await; + cx.assert_shared_state(indoc! { + "The «quickˇ» brown + fox «jumpsˇ» over + the «lazy ˇ»dog" + }) + .await; + + // toggling through visual mode works as expected + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! { + "The «quick brown + fox jumps over + the lazy ˇ»dog" + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v"]).await; + cx.assert_shared_state(indoc! { + "The «quickˇ» brown + fox «jumpsˇ» over + the «lazy ˇ»dog" + }) + .await; + + cx.set_shared_state(indoc! { + "The ˇquick + brown + fox + jumps over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"]) + .await; + cx.assert_shared_state(indoc! { + "The«ˇ q»uick + bro«ˇwn» + foxˇ + jumps over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["down"]).await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick + brow«nˇ» + fox + jump«sˇ» over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystroke("left").await; + cx.assert_shared_state(indoc! { + "The«ˇ q»uick + bro«ˇwn» + foxˇ + jum«ˇps» over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["s", "o", "escape"]).await; + cx.assert_shared_state(indoc! { + "Theˇouick + broo + foxo + jumo over the + + lazy dog + " + }) + .await; + } + + #[gpui::test] + async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "ˇThe quick brown + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; + cx.assert_shared_state(indoc! { + "«Tˇ»he quick brown + «fˇ»ox jumps over + «tˇ»he lazy dog + ˇ" + }) + .await; + + cx.simulate_shared_keystrokes(["shift-i", "k", "escape"]) + .await; + cx.assert_shared_state(indoc! { + "ˇkThe quick brown + kfox jumps over + kthe lazy dog + k" + }) + .await; + + cx.set_shared_state(indoc! { + "ˇThe quick brown + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; + cx.assert_shared_state(indoc! { + "«Tˇ»he quick brown + «fˇ»ox jumps over + «tˇ»he lazy dog + ˇ" + }) + .await; + cx.simulate_shared_keystrokes(["c", "k", "escape"]).await; + cx.assert_shared_state(indoc! { + "ˇkhe quick brown + kox jumps over + khe lazy dog + k" + }) + .await; + } + + #[gpui::test] + async fn test_visual_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("hello (in [parˇens] o)").await; + cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await; + cx.simulate_shared_keystrokes(["a", "]"]).await; + cx.assert_shared_state("hello (in «[parens]ˇ» o)").await; + assert_eq!(cx.mode(), Mode::Visual); + cx.simulate_shared_keystrokes(["i", "("]).await; + cx.assert_shared_state("hello («in [parens] oˇ»)").await; + + cx.set_shared_state("hello in a wˇord again.").await; + cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"]) + .await; + cx.assert_shared_state("hello in a w«ordˇ» again.").await; + assert_eq!(cx.mode(), Mode::VisualBlock); + cx.simulate_shared_keystrokes(["o", "a", "s"]).await; + cx.assert_shared_state("«ˇhello in a word» again.").await; + assert_eq!(cx.mode(), Mode::Visual); + } + + #[gpui::test] + async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbc", Mode::Normal); + cx.simulate_keystrokes(["ctrl-v"]); + assert_eq!(cx.mode(), Mode::VisualBlock); + cx.simulate_keystrokes(["cmd-shift-p", "escape"]); + assert_eq!(cx.mode(), Mode::VisualBlock); + } } diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json index 6769145412..bf14ae2495 100644 --- a/crates/vim/test_data/test_enter_visual_line_mode.json +++ b/crates/vim/test_data/test_enter_visual_line_mode.json @@ -1,15 +1,15 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"shift-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} {"Put":{"state":"a\nˇ\nb"}} {"Key":"shift-v"} -{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"a\nˇb","mode":"Normal"}} {"Put":{"state":"a\nb\nˇ"}} {"Key":"shift-v"} -{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index 4fdb4c7667..090e35cc5d 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -1,20 +1,20 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} {"Key":"w"} {"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}} {"Key":"escape"} {"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} {"Key":"v"} {"Key":"k"} {"Key":"b"} -{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}} {"Put":{"state":"a\nˇ\nb\n"}} {"Key":"v"} -{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}} {"Key":"v"} {"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} {"Put":{"state":"a\nb\nˇ"}} {"Key":"v"} -{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"a\nb\nˇ","mode":"Visual"}} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index f683c0a314..cff3ab80e2 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -2,9 +2,9 @@ {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":"Visual"}} {"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_block_insert.json b/crates/vim/test_data/test_visual_block_insert.json new file mode 100644 index 0000000000..d3d2689bd3 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_insert.json @@ -0,0 +1,18 @@ +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"9"} +{"Key":"down"} +{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} +{"Key":"shift-i"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"9"} +{"Key":"down"} +{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} +{"Key":"c"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json new file mode 100644 index 0000000000..ac306de4ab --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -0,0 +1,32 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}} +{"Key":"2"} +{"Key":"down"} +{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}} +{"Key":"e"} +{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}} +{"Key":"^"} +{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}} +{"Key":"$"} +{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}} +{"Key":"shift-f"} +{"Key":" "} +{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} +{"Key":"v"} +{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}} +{"Key":"ctrl-v"} +{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} +{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"down"} +{"Key":"down"} +{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}} +{"Key":"down"} +{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}} +{"Key":"left"} +{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}} +{"Key":"s"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_delete.json b/crates/vim/test_data/test_visual_delete.json index df025f48a0..d9f8055600 100644 --- a/crates/vim/test_data/test_visual_delete.json +++ b/crates/vim/test_data/test_visual_delete.json @@ -1,7 +1,7 @@ {"Put":{"state":"The quick ˇbrown"}} {"Key":"v"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown"}} {"Key":"v"} {"Key":"w"} diff --git a/crates/vim/test_data/test_visual_object.json b/crates/vim/test_data/test_visual_object.json new file mode 100644 index 0000000000..7c95a8dc73 --- /dev/null +++ b/crates/vim/test_data/test_visual_object.json @@ -0,0 +1,19 @@ +{"Put":{"state":"hello (in [parˇens] o)"}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"a"} +{"Key":"]"} +{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}} +{"Key":"i"} +{"Key":"("} +{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}} +{"Put":{"state":"hello in a wˇord again."}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}} +{"Key":"o"} +{"Key":"a"} +{"Key":"s"} +{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index b1c43bf9a2..0041baf969 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,236 +1,236 @@ {"Put":{"state":"The quick ˇbrown\nfox"}} {"Key":"v"} -{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}}