diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c9df3be659..7e2fbd82de 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -246,9 +246,10 @@ "displayLines": true } ], + "g v": "vim::RestoreVisualSelection", "g ]": "editor::GoToDiagnostic", "g [": "editor::GoToPrevDiagnostic", - "g i": ["workspace::SendKeystrokes", "` ^ i"], + "g i": "vim::InsertAtPrevious", "g ,": "vim::ChangeListNewer", "g ;": "vim::ChangeListOlder", "shift-h": "vim::WindowTop", diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 972a595e06..6e041d3b70 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -450,6 +450,7 @@ impl PromptLibrary { editor.set_show_gutter(false, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); + editor.set_use_modal_editing(false); editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); editor.set_completion_provider(Box::new( SlashCommandCompletionProvider::new(commands, None, None), diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 3ebd4eece2..cc268289f5 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,4 +1,8 @@ -use crate::{normal::repeat, state::Mode, Vim}; +use crate::{ + normal::{mark::create_mark, repeat}, + state::Mode, + Vim, +}; use editor::{scroll::Autoscroll, Bias}; use gpui::{actions, Action, ViewContext}; use language::SelectionGoal; @@ -15,6 +19,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext< let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording_immediately(action.boxed_clone()); if count <= 1 || vim.workspace_state.replaying { + create_mark(vim, "^".into(), false, cx); vim.update_active_editor(cx, |_, editor, cx| { editor.dismiss_menus_and_popups(false, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 02729e04bb..0bc03b1637 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -51,6 +51,7 @@ actions!( InsertEndOfLine, InsertLineAbove, InsertLineBelow, + InsertAtPrevious, DeleteLeft, DeleteRight, ChangeToEndOfLine, @@ -73,6 +74,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.start_recording(cx); + vim.switch_mode(Mode::Insert, false, cx); + vim.update_active_editor(cx, |vim, editor, cx| { + if let Some(marks) = vim.state().marks.get("^") { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark)) + }); + } + }); + }); +} + fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.start_recording(cx); diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 840528fddf..eb9dcb2a8c 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -11,6 +11,7 @@ use language::SelectionGoal; use crate::{ motion::{self, Motion}, + state::Mode, Vim, }; @@ -29,41 +30,32 @@ pub fn create_mark(vim: &mut Vim, text: Arc, tail: bool, cx: &mut WindowCon vim.clear_operator(cx); } -pub fn create_mark_after(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { - let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| { +pub fn create_visual_marks(vim: &mut Vim, mode: Mode, cx: &mut WindowContext) { + let mut starts = vec![]; + let mut ends = vec![]; + let mut reversed = vec![]; + + vim.update_active_editor(cx, |_, editor, cx| { let (map, selections) = editor.selections.all_display(cx); - selections - .into_iter() - .map(|selection| { - let point = movement::saturating_right(&map, selection.tail()); + for selection in selections { + let end = movement::saturating_left(&map, selection.end); + ends.push( map.buffer_snapshot - .anchor_before(point.to_offset(&map, Bias::Left)) - }) - .collect::>() - }) else { - return; - }; - - vim.update_state(|state| state.marks.insert(text.to_string(), anchors)); - vim.clear_operator(cx); -} - -pub fn create_mark_before(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { - let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| { - let (map, selections) = editor.selections.all_display(cx); - selections - .into_iter() - .map(|selection| { - let point = movement::saturating_left(&map, selection.head()); + .anchor_before(end.to_offset(&map, Bias::Left)), + ); + starts.push( map.buffer_snapshot - .anchor_before(point.to_offset(&map, Bias::Left)) - }) - .collect::>() - }) else { - return; - }; + .anchor_after(selection.start.to_offset(&map, Bias::Right)), + ); + reversed.push(selection.reversed) + } + }); - vim.update_state(|state| state.marks.insert(text.to_string(), anchors)); + vim.update_state(|state| { + state.marks.insert("<".to_string(), starts); + state.marks.insert(">".to_string(), ends); + state.stored_visual_mode.replace((mode, reversed)); + }); vim.clear_operator(cx); } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index b822626dfa..082e78136b 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -84,6 +84,7 @@ pub struct EditorState { pub replacements: Vec<(Range, String)>, pub marks: HashMap>, + pub stored_visual_mode: Option<(Mode, Vec)>, pub change_list: Vec>, pub change_list_position: Option, diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 7a2d6fa5e8..5340310270 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1127,6 +1127,26 @@ async fn test_lt_gt_marks(cx: &mut TestAppContext) { Line five " }); + + cx.simulate_shared_keystrokes("v i w o escape").await; + cx.simulate_shared_keystrokes("` >").await; + cx.shared_state().await.assert_eq(indoc! {" + Line one + Line two + Line three + Line fouˇr + Line five + " + }); + cx.simulate_shared_keystrokes("` <").await; + cx.shared_state().await.assert_eq(indoc! {" + Line one + Line two + Line three + Line ˇfour + Line five + " + }); } #[gpui::test] @@ -1166,4 +1186,14 @@ async fn test_caret_mark(cx: &mut TestAppContext) { Line five " }); + + cx.simulate_shared_keystrokes("k a ! escape k g i ?").await; + cx.shared_state().await.assert_eq(indoc! {" + Line one + Line two + Line three!?ˇ + Straight thing four + Line five + " + }); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8eb3ba8626..0ed33bce17 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -31,10 +31,7 @@ use gpui::{ use language::{CursorShape, Point, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; -use normal::{ - mark::{create_mark, create_mark_after, create_mark_before}, - normal_replace, -}; +use normal::{mark::create_visual_marks, normal_replace}; use replace::multi_replace; use schemars::JsonSchema; use serde::Deserialize; @@ -431,8 +428,8 @@ impl Vim { // Sync editor settings like clip mode self.sync_vim_settings(cx); - if mode != Mode::Insert && last_mode == Mode::Insert { - create_mark_after(self, "^".into(), cx) + if !mode.is_visual() && last_mode.is_visual() { + create_visual_marks(self, last_mode, cx); } if leave_selections { @@ -790,7 +787,6 @@ impl Vim { let is_multicursor = editor.read(cx).selections.count() > 1; let state = self.state(); - let mut is_visual = state.mode.is_visual(); if state.mode == Mode::Insert && state.current_tx.is_some() { if state.current_anchor.is_none() { self.update_state(|state| state.current_anchor = Some(newest)); @@ -807,18 +803,11 @@ impl Vim { } else { self.switch_mode(Mode::Visual, false, cx) } - is_visual = true; } else if newest.start == newest.end && !is_multicursor && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode) { self.switch_mode(Mode::Normal, true, cx); - is_visual = false; - } - - if is_visual { - create_mark_before(self, ">".into(), cx); - create_mark(self, "<".into(), true, cx) } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index cc320bc207..e6f5d29560 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -5,7 +5,7 @@ use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement, scroll::Autoscroll, - Bias, DisplayPoint, Editor, + Bias, DisplayPoint, Editor, ToOffset, }; use gpui::{actions, ViewContext, WindowContext}; use language::{Point, Selection, SelectionGoal}; @@ -16,8 +16,8 @@ use workspace::{searchable::Direction, Workspace}; use crate::{ motion::{start_of_line, Motion}, - normal::substitute::substitute, normal::yank::{copy_selections_content, yank_selections_content}, + normal::{mark::create_visual_marks, substitute::substitute}, object::Object, state::{Mode, Operator}, Vim, @@ -37,6 +37,7 @@ actions!( SelectPrevious, SelectNextMatch, SelectPreviousMatch, + RestoreVisualSelection, ] ); @@ -83,6 +84,52 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { select_match(workspace, vim, Direction::Prev, cx); }); }); + + workspace.register_action(|_, _: &RestoreVisualSelection, cx| { + Vim::update(cx, |vim, cx| { + let Some((stored_mode, reversed)) = + vim.update_state(|state| state.stored_visual_mode.take()) + else { + return; + }; + let Some((start, end)) = vim.state().marks.get("<").zip(vim.state().marks.get(">")) + else { + return; + }; + let ranges = start + .into_iter() + .zip(end) + .zip(reversed) + .map(|((start, end), reversed)| (*start, *end, reversed)) + .collect::>(); + + if vim.state().mode.is_visual() { + create_visual_marks(vim, vim.state().mode, cx); + } + + vim.update_active_editor(cx, |_, editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let map = s.display_map(); + let ranges = ranges + .into_iter() + .map(|(start, end, reversed)| { + let new_end = + movement::saturating_right(&map, end.to_display_point(&map)); + Selection { + id: s.new_selection_id(), + start: start.to_offset(&map.buffer_snapshot), + end: new_end.to_offset(&map, Bias::Left), + reversed, + goal: SelectionGoal::None, + } + }) + .collect(); + s.select(ranges); + }) + }); + vim.switch_mode(stored_mode, true, cx) + }); + }); } pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { @@ -483,6 +530,7 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext"); + cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual); + } + #[gpui::test] async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; @@ -1247,4 +1306,56 @@ mod test { " }); } + + #[gpui::test] + async fn test_gv(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown" + }) + .await; + cx.simulate_shared_keystrokes("v i w escape g v").await; + cx.shared_state().await.assert_eq(indoc! { + "The «quickˇ» brown" + }); + + cx.simulate_shared_keystrokes("o escape g v").await; + cx.shared_state().await.assert_eq(indoc! { + "The «ˇquick» brown" + }); + + cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await; + cx.shared_state().await.assert_eq(indoc! { + "«Thˇ»e quick brown" + }); + cx.simulate_shared_keystrokes("g v").await; + cx.shared_state().await.assert_eq(indoc! { + "The «ˇquick» brown" + }); + cx.simulate_shared_keystrokes("g v").await; + cx.shared_state().await.assert_eq(indoc! { + "«Thˇ»e quick brown" + }); + + cx.set_state( + indoc! {" + fiˇsh one + fish two + fish red + fish blue + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("4 g l escape escape g v"); + cx.assert_state( + indoc! {" + «fishˇ» one + «fishˇ» two + «fishˇ» red + «fishˇ» blue + "}, + Mode::Visual, + ); + } } diff --git a/crates/vim/test_data/test_caret_mark.json b/crates/vim/test_data/test_caret_mark.json index 9ef77d945c..6a117e9968 100644 --- a/crates/vim/test_data/test_caret_mark.json +++ b/crates/vim/test_data/test_caret_mark.json @@ -24,3 +24,12 @@ {"Key":"`"} {"Key":"^"} {"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}} +{"Key":"k"} +{"Key":"a"} +{"Key":"!"} +{"Key":"escape"} +{"Key":"k"} +{"Key":"g"} +{"Key":"i"} +{"Key":"?"} +{"Get":{"state":"Line one\nLine two\nLine three!?ˇ\nStraight thing four\nLine five\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_gv.json b/crates/vim/test_data/test_gv.json new file mode 100644 index 0000000000..7d33535098 --- /dev/null +++ b/crates/vim/test_data/test_gv.json @@ -0,0 +1,24 @@ +{"Put":{"state":"The ˇquick brown"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"w"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"v"} +{"Get":{"state":"The «quickˇ» brown","mode":"Visual"}} +{"Key":"o"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"v"} +{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} +{"Key":"escape"} +{"Key":"^"} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} +{"Key":"g"} +{"Key":"v"} +{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} +{"Key":"g"} +{"Key":"v"} +{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_lt_gt_marks.json b/crates/vim/test_data/test_lt_gt_marks.json index acd750dadd..142ceb9b95 100644 --- a/crates/vim/test_data/test_lt_gt_marks.json +++ b/crates/vim/test_data/test_lt_gt_marks.json @@ -16,3 +16,14 @@ {"Key":"`"} {"Key":">"} {"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"w"} +{"Key":"o"} +{"Key":"escape"} +{"Key":"`"} +{"Key":">"} +{"Get":{"state":"Line one\nLine two\nLine three\nLine fouˇr\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"<"} +{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}}