Store some vim state per-editor

This fixes a bug where opening and closing command would reset your
selection incorrectly.
This commit is contained in:
Conrad Irwin 2023-08-17 13:35:32 -06:00
parent d308c91020
commit 3514816ece
12 changed files with 106 additions and 48 deletions

View file

@ -1,4 +1,4 @@
use crate::Vim; use crate::{Vim, VimEvent};
use editor::{EditorBlurred, EditorFocused, EditorReleased}; use editor::{EditorBlurred, EditorFocused, EditorReleased};
use gpui::AppContext; use gpui::AppContext;
@ -22,6 +22,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
editor.window().update(cx, |cx| { editor.window().update(cx, |cx| {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.set_active_editor(editor.clone(), 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.active_editor = None;
} }
} }
vim.editor_states.remove(&editor.id())
}); });
}); });
} }

View file

@ -34,7 +34,7 @@ impl ModeIndicator {
if settings::get::<VimModeSetting>(cx).0 { if settings::get::<VimModeSetting>(cx).0 {
mode_indicator.mode = cx mode_indicator.mode = cx
.has_global::<Vim>() .has_global::<Vim>()
.then(|| cx.global::<Vim>().state.mode); .then(|| cx.global::<Vim>().state().mode);
} else { } else {
mode_indicator.mode.take(); mode_indicator.mode.take();
} }
@ -46,7 +46,7 @@ impl ModeIndicator {
.has_global::<Vim>() .has_global::<Vim>()
.then(|| { .then(|| {
let vim = cx.global::<Vim>(); let vim = cx.global::<Vim>();
vim.enabled.then(|| vim.state.mode) vim.enabled.then(|| vim.state().mode)
}) })
.flatten(); .flatten();

View file

@ -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 times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
let operator = Vim::read(cx).active_operator(); 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::Normal => normal_motion(motion, operator, times, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
Mode::Insert => { Mode::Insert => {
@ -158,7 +158,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
} }
fn repeat_motion(backwards: bool, 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 }) => { Some(Motion::FindForward { before, text }) => {
if backwards { if backwards {
Motion::FindBackward { Motion::FindBackward {

View file

@ -116,8 +116,8 @@ pub fn normal_motion(
pub fn normal_object(object: Object, cx: &mut WindowContext) { pub fn normal_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
match vim.state.operator_stack.pop() { match vim.maybe_pop_operator() {
Some(Operator::Object { around }) => match vim.state.operator_stack.pop() { Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
Some(Operator::Change) => change_object(vim, object, around, cx), Some(Operator::Change) => change_object(vim, object, around, cx),
Some(Operator::Delete) => delete_object(vim, object, around, cx), Some(Operator::Delete) => delete_object(vim, object, around, cx),
Some(Operator::Yank) => yank_object(vim, object, around, cx), Some(Operator::Yank) => yank_object(vim, object, around, cx),

View file

@ -13,7 +13,7 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
let mut cursor_positions = Vec::new(); let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) { for selection in editor.selections.all::<Point>(cx) {
match vim.state.mode { match vim.state().mode {
Mode::VisualLine => { Mode::VisualLine => {
let start = Point::new(selection.start.row, 0); let start = Point::new(selection.start.row, 0);
let end = let end =

View file

@ -70,10 +70,10 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
cx, cx,
); );
} }
vim.state.search = SearchState { vim.workspace_state.search = SearchState {
direction, direction,
count, count,
initial_query: query, initial_query: query.clone(),
}; };
}); });
} }
@ -83,7 +83,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
// hook into the existing to clear out any vim search state on cmd+f or edit -> find. // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) { fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
Vim::update(cx, |vim, _| vim.state.search = Default::default()); Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
cx.propagate_action(); cx.propagate_action();
} }
@ -93,8 +93,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| { 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 mut count = state.count;
let direction = state.direction;
// in the case that the query has changed, the search bar // in the case that the query has changed, the search bar
// will have selected the next match already. // 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) count = count.saturating_sub(1)
} }
search_bar.select_match(state.direction, count, cx);
state.count = 1; state.count = 1;
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx); search_bar.focus_editor(&Default::default(), cx);
}); });
} }

View file

@ -4,7 +4,7 @@ use language::Point;
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) { pub fn substitute(vim: &mut Vim, count: Option<usize>, 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.switch_mode(Mode::Insert, true, cx);
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {

View file

@ -62,7 +62,7 @@ pub fn init(cx: &mut AppContext) {
} }
fn object(object: Object, cx: &mut WindowContext) { 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::Normal => normal_object(object, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
Mode::Insert => { Mode::Insert => {

View file

@ -41,16 +41,20 @@ pub enum Operator {
FindBackward { after: bool }, FindBackward { after: bool },
} }
#[derive(Default)] #[derive(Default, Clone)]
pub struct VimState { pub struct EditorState {
pub mode: Mode, pub mode: Mode,
pub last_mode: Mode, pub last_mode: Mode,
pub operator_stack: Vec<Operator>, pub operator_stack: Vec<Operator>,
pub search: SearchState, }
#[derive(Default, Clone)]
pub struct WorkspaceState {
pub search: SearchState,
pub last_find: Option<Motion>, pub last_find: Option<Motion>,
} }
#[derive(Clone)]
pub struct SearchState { pub struct SearchState {
pub direction: Direction, pub direction: Direction,
pub count: usize, pub count: usize,
@ -67,7 +71,7 @@ impl Default for SearchState {
} }
} }
impl VimState { impl EditorState {
pub fn cursor_shape(&self) -> CursorShape { pub fn cursor_shape(&self) -> CursorShape {
match self.mode { match self.mode {
Mode::Normal => { Mode::Normal => {

View file

@ -76,12 +76,12 @@ impl<'a> VimTestContext<'a> {
} }
pub fn mode(&mut self) -> Mode { pub fn mode(&mut self) -> Mode {
self.cx.read(|cx| cx.global::<Vim>().state.mode) self.cx.read(|cx| cx.global::<Vim>().state().mode)
} }
pub fn active_operator(&mut self) -> Option<Operator> { pub fn active_operator(&mut self) -> Option<Operator> {
self.cx self.cx
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied()) .read(|cx| cx.global::<Vim>().state().operator_stack.last().copied())
} }
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {

View file

@ -12,7 +12,7 @@ mod utils;
mod visual; mod visual;
use anyhow::Result; use anyhow::Result;
use collections::CommandPaletteFilter; use collections::{CommandPaletteFilter, HashMap};
use editor::{movement, Editor, EditorMode, Event}; use editor::{movement, Editor, EditorMode, Event};
use gpui::{ use gpui::{
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
@ -24,7 +24,7 @@ use motion::Motion;
use normal::normal_replace; use normal::normal_replace;
use serde::Deserialize; use serde::Deserialize;
use settings::{Setting, SettingsStore}; use settings::{Setting, SettingsStore};
use state::{Mode, Operator, VimState}; use state::{EditorState, Mode, Operator, WorkspaceState};
use std::sync::Arc; use std::sync::Arc;
use visual::{visual_block_motion, visual_replace}; use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace}; use workspace::{self, Workspace};
@ -127,7 +127,9 @@ pub struct Vim {
active_editor: Option<WeakViewHandle<Editor>>, active_editor: Option<WeakViewHandle<Editor>>,
editor_subscription: Option<Subscription>, editor_subscription: Option<Subscription>,
enabled: bool, enabled: bool,
state: VimState, editor_states: HashMap<usize, EditorState>,
workspace_state: WorkspaceState,
default_state: EditorState,
} }
impl Vim { impl Vim {
@ -143,7 +145,7 @@ impl Vim {
} }
fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) { fn set_active_editor(&mut self, editor: ViewHandle<Editor>, 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 { self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
Event::SelectionsChanged { local: true } => { Event::SelectionsChanged { local: true } => {
let editor = editor.read(cx); let editor = editor.read(cx);
@ -163,7 +165,10 @@ impl Vim {
let editor_mode = editor.mode(); let editor_mode = editor.mode();
let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty(); let newest_selection_empty = editor.selections.newest::<usize>(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); 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) { fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
let last_mode = self.state.mode; let state = self.state();
let prior_mode = self.state.last_mode; let last_mode = state.mode;
self.state.last_mode = last_mode; let prior_mode = state.last_mode;
self.state.mode = mode; self.update_state(|state| {
self.state.operator_stack.clear(); state.last_mode = last_mode;
state.mode = mode;
state.operator_stack.clear();
});
cx.emit_global(VimEvent::ModeChanged { mode }); cx.emit_global(VimEvent::ModeChanged { mode });
@ -207,7 +215,9 @@ impl Vim {
// we cheat with visual block mode and use multiple cursors. // we cheat with visual block mode and use multiple cursors.
// the cost of this cheat is we need to convert back to a single // the cost of this cheat is we need to convert back to a single
// cursor whenever vim would. // 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 tail = s.oldest_anchor().tail();
let head = s.newest_anchor().head(); let head = s.newest_anchor().head();
s.select_anchor_ranges(vec![tail..head]); s.select_anchor_ranges(vec![tail..head]);
@ -237,7 +247,7 @@ impl Vim {
} }
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { 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); self.sync_vim_settings(cx);
} }
@ -250,9 +260,13 @@ impl Vim {
} }
} }
fn maybe_pop_operator(&mut self) -> Option<Operator> {
self.update_state(|state| state.operator_stack.pop())
}
fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator { fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
let popped_operator = self.state.operator_stack.pop() 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"); ) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
self.sync_vim_settings(cx); self.sync_vim_settings(cx);
popped_operator popped_operator
} }
@ -266,12 +280,12 @@ impl Vim {
} }
fn clear_operator(&mut self, cx: &mut WindowContext) { 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); self.sync_vim_settings(cx);
} }
fn active_operator(&self) -> Option<Operator> { fn active_operator(&self) -> Option<Operator> {
self.state.operator_stack.last().copied() self.state().operator_stack.last().copied()
} }
fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) { fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
@ -282,15 +296,19 @@ impl Vim {
match Vim::read(cx).active_operator() { match Vim::read(cx).active_operator() {
Some(Operator::FindForward { before }) => { Some(Operator::FindForward { before }) => {
let find = Motion::FindForward { before, text }; 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) motion::motion(find, cx)
} }
Some(Operator::FindBackward { after }) => { Some(Operator::FindBackward { after }) => {
let find = Motion::FindBackward { after, text }; 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) 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::Normal => normal_replace(text, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
_ => Vim::update(cx, |vim, cx| vim.clear_operator(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) { fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
if self.enabled != enabled { if self.enabled != enabled {
self.enabled = enabled; self.enabled = enabled;
self.state = Default::default();
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| { cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
if self.enabled { 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<T>(&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) { fn sync_vim_settings(&self, cx: &mut WindowContext) {
let state = &self.state; let state = self.state();
let cursor_shape = state.cursor_shape(); let cursor_shape = state.cursor_shape();
self.update_active_editor(cx, |editor, cx| { 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) { fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { 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) vim.switch_mode(Mode::Visual, false, cx)
} }
}) })

View file

@ -53,7 +53,7 @@ pub fn init(cx: &mut AppContext) {
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) { pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, 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); let is_up_or_down = matches!(motion, Motion::Up | Motion::Down);
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
motion.move_point(map, point, goal, times) motion.move_point(map, point, goal, times)
@ -85,7 +85,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
// ensure the current character is included in the selection. // ensure the current character is included in the selection.
if !selection.reversed { 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) movement::saturating_right(map, selection.end)
} else { } else {
movement::right(map, selection.end) 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<Workspace>) { fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
if vim.state.mode == mode { if vim.state().mode == mode {
vim.switch_mode(Mode::Normal, false, cx); vim.switch_mode(Mode::Normal, false, cx);
} else { } else {
vim.switch_mode(mode, false, cx); vim.switch_mode(mode, false, cx);
@ -294,7 +294,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal) selection.collapse_to(cursor, selection.goal)
}); });
if vim.state.mode == Mode::VisualBlock { if vim.state().mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()]) s.select_anchors(vec![s.first_anchor()])
} }
}); });
@ -313,7 +313,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
s.move_with(|_, selection| { s.move_with(|_, selection| {
selection.collapse_to(selection.start, SelectionGoal::None) 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()]) s.select_anchors(vec![s.first_anchor()])
} }
}); });
@ -971,4 +971,15 @@ mod test {
}) })
.await; .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);
}
} }