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); + } }