From 1cc0798aea36f0587ac93bf96857d495501a3761 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 15 Aug 2023 08:48:01 -0600 Subject: [PATCH 01/12] Add a VisualBlock mode Instead of trying to extend the Mode::Visual special case, just split out into three different modes. --- crates/vim/src/mode_indicator.rs | 5 +- crates/vim/src/motion.rs | 2 +- crates/vim/src/normal/case.rs | 4 +- crates/vim/src/normal/substitute.rs | 4 +- crates/vim/src/object.rs | 2 +- crates/vim/src/state.rs | 12 ++- crates/vim/src/test.rs | 2 +- .../src/test/neovim_backed_test_context.rs | 2 +- crates/vim/src/test/neovim_connection.rs | 7 +- crates/vim/src/vim.rs | 8 +- crates/vim/src/visual.rs | 18 ++-- .../test_enter_visual_line_mode.json | 6 +- .../vim/test_data/test_enter_visual_mode.json | 10 +- ...ltiline_surrounding_character_objects.json | 4 +- crates/vim/test_data/test_visual_delete.json | 2 +- .../test_data/test_visual_word_object.json | 96 +++++++++---------- 16 files changed, 94 insertions(+), 90 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 48cae9f4ae..79a3bbd051 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -86,8 +86,9 @@ impl View for ModeIndicator { 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..e04457d65c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -149,7 +149,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { let operator = Vim::read(cx).active_operator(); 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 } diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index b3e101262d..0ec0eeba84 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -14,14 +14,14 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext(cx) { match vim.state.mode { - Mode::Visual { line: true } => { + 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/substitute.rs b/crates/vim/src/normal/substitute.rs index d2429433fe..cf4e5a63d6 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,7 +4,7 @@ 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 }; + let line_mode = vim.state.mode == Mode::VisualLine; vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -52,7 +52,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..e1e21e4e3b 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -64,7 +64,7 @@ pub fn init(cx: &mut AppContext) { fn object(object: Object, cx: &mut WindowContext) { 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 } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 905bd5fd2a..b38dac4aa8 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, } } } @@ -74,7 +76,7 @@ impl VimState { CursorShape::Underscore } } - Mode::Visual { .. } => CursorShape::Block, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -89,7 +91,7 @@ impl VimState { 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 +103,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..1c7559e440 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 }; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index dd9be10723..e983d5ceec 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -261,8 +261,9 @@ 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), + "CTRL-V" => Some(Mode::VisualBlock), _ => None, }; @@ -270,7 +271,7 @@ impl NeovimConnection { // 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::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; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e8d69d696c..038e47659d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -164,7 +164,7 @@ impl Vim { 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); + self.switch_mode(Mode::Visual, true, cx); } } @@ -270,7 +270,7 @@ impl Vim { } 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)), }, _ => {} @@ -317,7 +317,7 @@ 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.selections.line_mode = matches!(state.mode, Mode::VisualLine); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); } else { @@ -368,7 +368,7 @@ impl Setting for VimModeSetting { fn local_selections_changed(newest_empty: bool, 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) + vim.switch_mode(Mode::Visual, false, cx) } }) } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1716e2d1a5..e5f9d8c459 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -138,10 +138,10 @@ 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::Normal | Mode::Insert | Mode::VisualLine | Mode::VisualBlock => { + vim.switch_mode(Mode::Visual, false, cx); } - Mode::Visual { line: false } => { + Mode::Visual => { vim.switch_mode(Mode::Normal, false, cx); } }) @@ -153,10 +153,10 @@ pub fn toggle_visual_line( 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::Normal | Mode::Insert | Mode::Visual | Mode::VisualBlock => { + vim.switch_mode(Mode::VisualLine, false, cx); } - Mode::Visual { line: true } => { + Mode::VisualLine => { vim.switch_mode(Mode::Normal, false, cx); } }) @@ -701,7 +701,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 +725,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 +738,7 @@ mod test { indoc! {" The quick brown the «lazyˇ» dog"}, - Mode::Visual { line: false }, + Mode::Visual, ); cx.simulate_keystroke("p"); cx.assert_state( 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_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_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"}} From 1b4dd49b1d8618e8149db3a28dbb398bdef1a52c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 15 Aug 2023 13:26:04 -0600 Subject: [PATCH 02/12] Vim visual block mode This isn't quite an exact emulation, as instead of using one selection that is magically in "column mode", we emulate it with a bunch of zed multi-selections (one per line). I think this is better, as it requires fewer changes to the codebase, and lets you see the impact of any changes immediately on all lines. Fixes: zed-industries/community#984 --- assets/keymaps/vim.json | 1 + crates/editor/src/selections_collection.rs | 20 +- crates/vim/src/object.rs | 40 +- crates/vim/src/state.rs | 1 + .../src/test/neovim_backed_test_context.rs | 11 +- crates/vim/src/test/neovim_connection.rs | 171 ++++++--- crates/vim/src/vim.rs | 24 +- crates/vim/src/visual.rs | 354 +++++++++++++++--- .../vim/test_data/test_visual_block_mode.json | 31 ++ 9 files changed, 518 insertions(+), 135 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_mode.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 02c09b33af..fc54934f2b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -103,6 +103,7 @@ ], "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 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/object.rs b/crates/vim/src/object.rs index e1e21e4e3b..37476caed5 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -72,6 +72,18 @@ 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 range( self, map: &DisplaySnapshot, @@ -87,13 +99,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 b38dac4aa8..66aaec02b9 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -44,6 +44,7 @@ pub enum Operator { #[derive(Default)] pub struct VimState { pub mode: Mode, + pub last_mode: Mode, pub operator_stack: Vec, pub search: SearchState, diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 1c7559e440..263692b36e 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -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 e983d5ceec..ddeb26164b 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() @@ -263,14 +271,48 @@ impl NeovimConnection { "n" => Some(Mode::Normal), "v" => Some(Mode::Visual), "V" => Some(Mode::VisualLine), - "CTRL-V" => Some(Mode::VisualBlock), + "\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::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 = @@ -291,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 } @@ -422,51 +463,63 @@ 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::>(); + let ret = util::test::generate_marked_text(text, &byte_ranges[..], true); + ret } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 038e47659d..df35e951d2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -26,7 +26,7 @@ use serde::Deserialize; use settings::{Setting, SettingsStore}; use state::{Mode, Operator, VimState}; use std::sync::Arc; -use visual::visual_replace; +use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; struct VimModeSetting(bool); @@ -182,6 +182,8 @@ impl Vim { fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { let last_mode = self.state.mode; + let prior_mode = self.state.last_mode; + self.state.last_mode = last_mode; self.state.mode = mode; self.state.operator_stack.clear(); @@ -196,7 +198,27 @@ 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 { + 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(); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index e5f9d8c459..cb4d865dc9 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -2,10 +2,13 @@ use std::{borrow::Cow, 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,55 +53,169 @@ 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 && !(head.column() == 0 && head == map.max_point()) { + head = movement::saturating_left(map, head); + } + + let Some((new_head, new_goal)) = move_selection(&map, head, goal) else { + return + }; + head = new_head; + if goal == SelectionGoal::None { + goal = new_goal; + } + + let mut 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 { + head = movement::saturating_right(map, head) + } + + if !preserve_goal + || !matches!( + goal, + SelectionGoal::ColumnRange { .. } | SelectionGoal::Column(_) + ) + { + goal = SelectionGoal::ColumnRange { + start: tail.column(), + end: head.column(), + } + } + + let mut columns = if let SelectionGoal::ColumnRange { start, end } = goal { + if start > end { + is_reversed = true; + end..start + } else { + is_reversed = false; + start..end + } + } else if let SelectionGoal::Column(column) = goal { + is_reversed = false; + column..(column + 1) + } else { + unreachable!() + }; + + if columns.start >= map.line_len(head.row()) { + columns.start = map.line_len(head.row()).saturating_sub(1); + } + if columns.start >= map.line_len(tail.row()) { + columns.start = map.line_len(tail.row()).saturating_sub(1); + } + + 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 mut 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() { @@ -136,28 +263,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::VisualLine | Mode::VisualBlock => { - vim.switch_mode(Mode::Visual, false, cx); - } - Mode::Visual => { - 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 | Mode::VisualBlock => { - vim.switch_mode(Mode::VisualLine, false, cx); - } - Mode::VisualLine => { +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); } }) } @@ -207,6 +318,9 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, 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); @@ -275,7 +392,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext linewise = all_selections_were_entire_line; } - let mut selection = selection.clone(); + let selection = selection.clone(); if !selection.reversed { let adjusted = selection.end; // If the selection is empty, move both the start and end forward one @@ -751,4 +868,119 @@ 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", "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; + } } 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..743f7fa76c --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -0,0 +1,31 @@ +{"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"} +{"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"}} From 7f06191c9fc781096415da80af8c5121a588cbfe Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 16 Aug 2023 10:44:59 -0600 Subject: [PATCH 03/12] Disable autoindent in visual block insert mode --- crates/editor/src/editor.rs | 17 ++++++++++++++--- crates/editor/src/multi_buffer.rs | 1 + crates/vim/src/state.rs | 4 ++++ crates/vim/src/test/neovim_connection.rs | 3 +-- crates/vim/src/vim.rs | 2 ++ crates/vim/src/visual.rs | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 256ef2284c..59143e8a39 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -575,6 +575,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, @@ -1409,6 +1410,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(), @@ -1587,6 +1589,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; } @@ -1719,7 +1729,7 @@ impl Editor { } self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx) + buffer.edit(edits, self.autoindent_mode.clone(), cx) }); } @@ -2194,7 +2204,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); @@ -2504,6 +2514,7 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + dbg!("insert!"); self.insert_with_autoindent_mode( text, Some(AutoindentMode::Block { @@ -3003,7 +3014,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/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8417c411f2..df807c8f28 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -364,6 +364,7 @@ impl MultiBuffer { S: ToOffset, T: Into>, { + dbg!("edit", &autoindent_mode); if self.buffers.borrow().is_empty() { return; } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 66aaec02b9..5f146aa690 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -90,6 +90,10 @@ 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 | Mode::VisualLine | Mode::VisualBlock => false, diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index ddeb26164b..2c7a33909e 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -520,6 +520,5 @@ fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { byte_range }) .collect::>(); - let ret = util::test::generate_marked_text(text, &byte_ranges[..], true); - ret + util::test::generate_marked_text(text, &byte_ranges[..], true); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index df35e951d2..e3f7c7dd10 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -339,6 +339,7 @@ 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.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); @@ -355,6 +356,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 diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index cb4d865dc9..6dff69a6e4 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -192,7 +192,7 @@ pub fn visual_block_motion( 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 mut selection = Selection { + let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), end: end.to_point(map), @@ -392,7 +392,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext linewise = all_selections_were_entire_line; } - let selection = selection.clone(); + let mut selection = selection.clone(); if !selection.reversed { let adjusted = selection.end; // If the selection is empty, move both the start and end forward one From 7598030102772fe75e281a515f46a94a27e0f87c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 16 Aug 2023 12:03:29 -0600 Subject: [PATCH 04/12] Tidy-up --- crates/editor/src/editor.rs | 1 - crates/editor/src/movement.rs | 16 +-- crates/editor/src/multi_buffer.rs | 1 - crates/vim/src/test/neovim_connection.rs | 2 +- crates/vim/src/visual.rs | 118 ++++++++---------- .../vim/test_data/test_visual_block_mode.json | 1 + 6 files changed, 63 insertions(+), 76 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 59143e8a39..5875b558c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2514,7 +2514,6 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - dbg!("insert!"); self.insert_with_autoindent_mode( text, Some(AutoindentMode::Block { 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/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index df807c8f28..8417c411f2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -364,7 +364,6 @@ impl MultiBuffer { S: ToOffset, T: Into>, { - dbg!("edit", &autoindent_mode); if self.buffers.borrow().is_empty() { return; } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 2c7a33909e..fc677f032c 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -520,5 +520,5 @@ fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { byte_range }) .collect::>(); - util::test::generate_marked_text(text, &byte_ranges[..], true); + util::test::generate_marked_text(text, &byte_ranges[..], true) } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 6dff69a6e4..18eddad5ca 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -129,62 +129,37 @@ pub fn visual_block_motion( let was_reversed = tail.column() > head.column(); - if !was_reversed && !(head.column() == 0 && head == map.max_point()) { + if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } - let Some((new_head, new_goal)) = move_selection(&map, head, goal) else { + let Some((new_head, _)) = move_selection(&map, head, goal) else { return }; head = new_head; - if goal == SelectionGoal::None { - goal = new_goal; - } - let mut is_reversed = tail.column() > head.column(); + 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 { + if !is_reversed && !preserve_goal { head = movement::saturating_right(map, head) } - if !preserve_goal - || !matches!( - goal, - SelectionGoal::ColumnRange { .. } | SelectionGoal::Column(_) - ) - { - goal = SelectionGoal::ColumnRange { - start: tail.column(), - end: head.column(), - } - } - - let mut columns = if let SelectionGoal::ColumnRange { start, end } = goal { - if start > end { - is_reversed = true; - end..start - } else { - is_reversed = false; - start..end - } - } else if let SelectionGoal::Column(column) = goal { - is_reversed = false; - column..(column + 1) - } else { - unreachable!() + 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 }; - if columns.start >= map.line_len(head.row()) { - columns.start = map.line_len(head.row()).saturating_sub(1); - } - if columns.start >= map.line_len(tail.row()) { - columns.start = map.line_len(tail.row()).saturating_sub(1); - } - + let columns = if is_reversed { + head.column()..tail.column() + } else { + tail.column()..head.column() + }; let mut selections = Vec::new(); let mut row = tail.row(); @@ -291,37 +266,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) }); - if vim.state.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); + }) }); vim.switch_mode(Mode::Normal, true, cx); }); @@ -948,8 +925,19 @@ mod test { " }) .await; - cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "down"]) + 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ˇ» diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index 743f7fa76c..ac306de4ab 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -21,6 +21,7 @@ {"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"} From d308c910205b300ec408dfc8629109de48ed3b0f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 11:21:58 -0600 Subject: [PATCH 05/12] Add I and A in visual block mode --- assets/keymaps/vim.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fc54934f2b..2864e5c1d2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -378,6 +378,11 @@ "s": "vim::Substitute", "c": "vim::Substitute", "~": "vim::ChangeCase", + "shift-i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-a": "vim::InsertAfter", "r": [ "vim::PushOperator", "Replace" From 3514816ecedd0bedcaa81f6f52296034d01d0f02 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 13:35:32 -0600 Subject: [PATCH 06/12] Store some vim state per-editor This fixes a bug where opening and closing command would reset your selection incorrectly. --- crates/vim/src/editor_events.rs | 6 +- crates/vim/src/mode_indicator.rs | 4 +- crates/vim/src/motion.rs | 4 +- crates/vim/src/normal.rs | 4 +- crates/vim/src/normal/case.rs | 2 +- crates/vim/src/normal/search.rs | 11 ++-- crates/vim/src/normal/substitute.rs | 2 +- crates/vim/src/object.rs | 2 +- crates/vim/src/state.rs | 12 ++-- crates/vim/src/test/vim_test_context.rs | 4 +- crates/vim/src/vim.rs | 82 ++++++++++++++++++------- crates/vim/src/visual.rs | 21 +++++-- 12 files changed, 106 insertions(+), 48 deletions(-) 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 79a3bbd051..4b1ade7a22 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(); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e04457d65c..29a1ba7df8 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -147,7 +147,7 @@ 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 | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), Mode::Insert => { @@ -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 0ec0eeba84..90967949bb 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -13,7 +13,7 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext(cx) { - match vim.state.mode { + match vim.state().mode { Mode::VisualLine => { let start = Point::new(selection.start.row, 0); let end = diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 9375c4e78d..44b304392f 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -70,10 +70,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(); } @@ -93,8 +93,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. @@ -103,8 +104,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 cf4e5a63d6..bfd2af0481 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,7 +4,7 @@ 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::VisualLine; + let line_mode = vim.state().mode == Mode::VisualLine; vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 37476caed5..14166d2dff 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -62,7 +62,7 @@ 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 | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), Mode::Insert => { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 5f146aa690..aacd3d26e0 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -41,16 +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, @@ -67,7 +71,7 @@ impl Default for SearchState { } } -impl VimState { +impl EditorState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { Mode::Normal => { diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ab5d7382c7..f5136be036 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 e3f7c7dd10..ca22d25012 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ 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, @@ -24,7 +24,7 @@ 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_block_motion, visual_replace}; use workspace::{self, Workspace}; @@ -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,7 +145,7 @@ 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); @@ -163,7 +165,10 @@ 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 { + if editor_mode == EditorMode::Full + && !newest_selection_empty + && self.state().mode == Mode::Normal + { self.switch_mode(Mode::Visual, true, cx); } } @@ -181,11 +186,14 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { - let last_mode = self.state.mode; - let prior_mode = self.state.last_mode; - self.state.last_mode = last_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 }); @@ -207,7 +215,9 @@ impl Vim { // 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 { + 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]); @@ -237,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); } @@ -250,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 } @@ -266,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) { @@ -282,15 +296,19 @@ 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 | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), @@ -302,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 { @@ -329,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| { @@ -391,7 +429,7 @@ impl Setting for VimModeSetting { fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { + if vim.enabled && vim.state().mode == Mode::Normal && !newest_empty { vim.switch_mode(Mode::Visual, false, cx) } }) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 18eddad5ca..866086d538 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -53,7 +53,7 @@ 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| { - if vim.state.mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { + 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) @@ -85,7 +85,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { - let next_point = if vim.state.mode == Mode::VisualBlock { + let next_point = if vim.state().mode == Mode::VisualBlock { movement::saturating_right(map, selection.end) } else { movement::right(map, selection.end) @@ -240,7 +240,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { fn toggle_mode(mode: Mode, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - if vim.state.mode == mode { + if vim.state().mode == mode { vim.switch_mode(Mode::Normal, false, cx); } else { vim.switch_mode(mode, false, cx); @@ -294,7 +294,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) s.move_with(|_, selection| { selection.collapse_to(selection.start, SelectionGoal::None) }); - if vim.state.mode == Mode::VisualBlock { + if vim.state().mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); @@ -971,4 +971,15 @@ mod test { }) .await; } + + #[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); + } } From 59d1a5632f26b5cc6ee33b10855c867a32f5b9cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:15:00 -0600 Subject: [PATCH 07/12] Fix edge-cases in visual block insert --- crates/vim/src/normal/substitute.rs | 3 +- crates/vim/src/vim.rs | 2 +- crates/vim/src/visual.rs | 58 +++++++++++++++++++ .../test_data/test_visual_block_insert.json | 18 ++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_insert.json diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index bfd2af0481..1d53c6831c 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -5,8 +5,8 @@ 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::VisualLine; - vim.switch_mode(Mode::Insert, true, cx); 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)] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index ca22d25012..e41fab5495 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -232,7 +232,7 @@ impl Vim { 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) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 866086d538..4065657e59 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -157,9 +157,12 @@ pub fn visual_block_motion( 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(); @@ -972,6 +975,61 @@ mod test { .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_mode_across_command(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; 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"}} From eb0b2e60bb6c0ad727545663e6a048a537897044 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:40:27 -0600 Subject: [PATCH 08/12] Preserve line mode selection on undo This diverges from vim's behaviour (which collapses the cursor to a single point on undo). --- crates/vim/src/vim.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e41fab5495..da1c634682 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -18,7 +18,7 @@ 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; @@ -150,8 +150,8 @@ impl Vim { 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 } => { @@ -427,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, 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) + } } }) } From 3c483d85f7a353689442893969ff33a50d2a6a50 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:58:10 -0600 Subject: [PATCH 09/12] Scrolling should work in visual mode --- assets/keymaps/vim.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2864e5c1d2..5281ec4213 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -107,6 +107,14 @@ "*": "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", @@ -294,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" From 243d1664e524d679771a76be030a82ce413dd6d0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 16:01:19 -0600 Subject: [PATCH 10/12] shift-enter should also give a newline (reported as vim feedback, but really true of the editor too) --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 38ec8ffb40..7c18ec7012 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -172,6 +172,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", From d4276acab82fe2f2fdb2cfa0ccb4cb8c9d9248ca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 16:04:55 -0600 Subject: [PATCH 11/12] Give up on monospace indicator Changing mode almost always introduces the (1 selected) text in the status bar, so we may as well also keep the --'s for block and line mode. --- crates/vim/src/mode_indicator.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 4b1ade7a22..b110c39dc4 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -80,15 +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 => "-- VISUAL --", - Mode::VisualLine => "VISUAL LINE", - Mode::VisualBlock => "VISUAL BLOCK", + Mode::VisualLine => "-- VISUAL LINE --", + Mode::VisualBlock => "-- VISUAL BLOCK --", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() From b0ba0f885175b466ee8cf1db88e569b00d64800e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 17:03:54 -0600 Subject: [PATCH 12/12] Fix visual objects Adds 'a'/'i' in visual mode --- assets/keymaps/vim.json | 20 +++++++- crates/vim/src/object.rs | 29 +++++++++++ crates/vim/src/visual.rs | 54 +++++++++++++++----- crates/vim/test_data/test_visual_object.json | 19 +++++++ 4 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 crates/vim/test_data/test_visual_object.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5281ec4213..a93d8aa3ec 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -366,7 +366,7 @@ } }, { - "context": "Editor && vim_mode == visual && !VimWaiting", + "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", "bindings": { "u": "editor::Undo", "o": "vim::OtherEnd", @@ -400,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/vim/src/object.rs b/crates/vim/src/object.rs index 14166d2dff..c203a89f72 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -84,6 +84,35 @@ impl Object { | 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, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 4065657e59..df7c8cfa45 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, cmp, sync::Arc}; use collections::HashMap; use editor::{ @@ -198,6 +198,11 @@ 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| { @@ -213,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 { @@ -1030,6 +1036,28 @@ mod test { .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; 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"}}