Initialize the active editor when vim mode is enabled

Antonio Scandurra created

Instead of waiting for a focus event. This makes more tests pass.

Change summary

crates/sum_tree/src/sum_tree.rs         |  2 
crates/vim/src/editor_events.rs         | 86 ++++++++++++++------------
crates/vim/src/motion.rs                |  4 
crates/vim/src/normal.rs                | 10 +-
crates/vim/src/normal/change.rs         |  6 
crates/vim/src/normal/delete.rs         |  6 
crates/vim/src/normal/yank.rs           |  6 
crates/vim/src/object.rs                | 18 ++--
crates/vim/src/test/vim_test_context.rs | 15 +--
crates/vim/src/vim.rs                   | 72 ++++++++++++----------
crates/vim/src/visual.rs                |  8 +-
11 files changed, 122 insertions(+), 111 deletions(-)

Detailed changes

crates/sum_tree/src/sum_tree.rs 🔗

@@ -735,7 +735,7 @@ mod tests {
             .map_or(5, |o| o.parse().expect("invalid OPERATIONS variable"));
 
         for seed in starting_seed..(starting_seed + num_iterations) {
-            dbg!(seed);
+            eprintln!("seed = {}", seed);
             let mut rng = StdRng::seed_from_u64(seed);
 
             let rng = &mut rng;

crates/vim/src/editor_events.rs 🔗

@@ -1,5 +1,5 @@
 use editor::{EditorBlurred, EditorFocused, EditorMode, EditorReleased, Event};
-use gpui::AppContext;
+use gpui::{AppContext, WindowContext};
 
 use crate::{state::Mode, Vim};
 
@@ -10,65 +10,71 @@ pub fn init(cx: &mut AppContext) {
 }
 
 fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
-    Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |previously_active_editor, cx| {
-            Vim::unhook_vim_settings(previously_active_editor, cx);
-        });
+    cx.update_window(editor.window_id(), |cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.update_active_editor(cx, |previously_active_editor, cx| {
+                Vim::unhook_vim_settings(previously_active_editor, cx);
+            });
 
-        vim.active_editor = Some(editor.downgrade());
-        vim.editor_subscription = Some(cx.subscribe(editor, |editor, event, cx| match event {
-            Event::SelectionsChanged { local: true } => {
-                let editor = editor.read(cx);
-                if editor.leader_replica_id().is_none() {
-                    let newest_empty = editor.selections.newest::<usize>(cx).is_empty();
-                    local_selections_changed(newest_empty, cx);
+            vim.active_editor = Some(editor.downgrade());
+            vim.editor_subscription = Some(cx.subscribe(editor, |editor, event, cx| match event {
+                Event::SelectionsChanged { local: true } => {
+                    let editor = editor.read(cx);
+                    if editor.leader_replica_id().is_none() {
+                        let newest_empty = editor.selections.newest::<usize>(cx).is_empty();
+                        local_selections_changed(newest_empty, cx);
+                    }
                 }
-            }
-            Event::InputIgnored { text } => {
-                Vim::active_editor_input_ignored(text.clone(), cx);
-            }
-            _ => {}
-        }));
+                Event::InputIgnored { text } => {
+                    Vim::active_editor_input_ignored(text.clone(), cx);
+                }
+                _ => {}
+            }));
 
-        if vim.enabled {
-            let editor = editor.read(cx);
-            let editor_mode = editor.mode();
-            let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
+            if vim.enabled {
+                let editor = editor.read(cx);
+                let editor_mode = editor.mode();
+                let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
 
-            if editor_mode == EditorMode::Full && !newest_selection_empty {
-                vim.switch_mode(Mode::Visual { line: false }, true, cx);
+                if editor_mode == EditorMode::Full && !newest_selection_empty {
+                    vim.switch_mode(Mode::Visual { line: false }, true, cx);
+                }
             }
-        }
 
-        vim.sync_vim_settings(cx);
+            vim.sync_vim_settings(cx);
+        });
     });
 }
 
 fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
-    Vim::update(cx, |vim, cx| {
-        if let Some(previous_editor) = vim.active_editor.clone() {
-            if previous_editor == editor.clone() {
-                vim.active_editor = None;
+    cx.update_window(editor.window_id(), |cx| {
+        Vim::update(cx, |vim, cx| {
+            if let Some(previous_editor) = vim.active_editor.clone() {
+                if previous_editor == editor.clone() {
+                    vim.active_editor = None;
+                }
             }
-        }
 
-        cx.update_window(editor.window_id(), |cx| {
-            editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
+            cx.update_window(editor.window_id(), |cx| {
+                editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
+            });
         });
-    })
+    });
 }
 
 fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
-    cx.update_default_global(|vim: &mut Vim, _| {
-        if let Some(previous_editor) = vim.active_editor.clone() {
-            if previous_editor == editor.clone() {
-                vim.active_editor = None;
+    cx.update_window(editor.window_id(), |cx| {
+        cx.update_default_global(|vim: &mut Vim, _| {
+            if let Some(previous_editor) = vim.active_editor.clone() {
+                if previous_editor == editor.clone() {
+                    vim.active_editor = None;
+                }
             }
-        }
+        });
     });
 }
 
-fn local_selections_changed(newest_empty: bool, cx: &mut AppContext) {
+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)

crates/vim/src/motion.rs 🔗

@@ -5,7 +5,7 @@ use editor::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     movement, Bias, CharKind, DisplayPoint, ToOffset,
 };
-use gpui::{actions, impl_actions, AppContext};
+use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::{Point, Selection, SelectionGoal};
 use serde::Deserialize;
 use workspace::Workspace;
@@ -116,7 +116,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
 }
 
-pub(crate) fn motion(motion: Motion, cx: &mut AppContext) {
+pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
     if let Some(Operator::Namespace(_))
     | Some(Operator::FindForward { .. })
     | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()

crates/vim/src/normal.rs 🔗

@@ -16,7 +16,7 @@ use editor::{
     scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
     Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
 };
-use gpui::{actions, impl_actions, AppContext, ViewContext};
+use gpui::{actions, impl_actions, AppContext, ViewContext, WindowContext};
 use language::{AutoindentMode, Point, SelectionGoal};
 use log::error;
 use serde::Deserialize;
@@ -94,7 +94,7 @@ pub fn normal_motion(
     motion: Motion,
     operator: Option<Operator>,
     times: usize,
-    cx: &mut AppContext,
+    cx: &mut WindowContext,
 ) {
     Vim::update(cx, |vim, cx| {
         match operator {
@@ -110,7 +110,7 @@ pub fn normal_motion(
     });
 }
 
-pub fn normal_object(object: Object, cx: &mut AppContext) {
+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() {
@@ -129,7 +129,7 @@ pub fn normal_object(object: Object, cx: &mut AppContext) {
     })
 }
 
-fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppContext) {
+fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {
@@ -424,7 +424,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Edito
     }
 }
 
-pub(crate) fn normal_replace(text: Arc<str>, cx: &mut AppContext) {
+pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {

crates/vim/src/normal/change.rs 🔗

@@ -3,10 +3,10 @@ use editor::{
     char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind,
     DisplayPoint,
 };
-use gpui::AppContext;
+use gpui::WindowContext;
 use language::Selection;
 
-pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppContext) {
+pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
     // Some motions ignore failure when switching to normal mode
     let mut motion_succeeded = matches!(
         motion,
@@ -38,7 +38,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppCo
     }
 }
 
-pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut AppContext) {
+pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
     let mut objects_found = false;
     vim.update_active_editor(cx, |editor, cx| {
         // We are swapping to insert mode anyway. Just set the line end clipping behavior now

crates/vim/src/normal/delete.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
 use collections::{HashMap, HashSet};
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
-use gpui::AppContext;
+use gpui::WindowContext;
 
-pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppContext) {
+pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -36,7 +36,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppCo
     });
 }
 
-pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut AppContext) {
+pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);

crates/vim/src/normal/yank.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
 use collections::HashMap;
-use gpui::AppContext;
+use gpui::WindowContext;
 
-pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppContext) {
+pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -25,7 +25,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut AppCont
     });
 }
 
-pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut AppContext) {
+pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);

crates/vim/src/object.rs 🔗

@@ -1,7 +1,7 @@
 use std::ops::Range;
 
 use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
-use gpui::{actions, impl_actions, AppContext};
+use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::Selection;
 use serde::Deserialize;
 use workspace::Workspace;
@@ -61,7 +61,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
 }
 
-fn object(object: Object, 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),
@@ -434,17 +434,17 @@ mod test {
     use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
 
     const WORD_LOCATIONS: &'static str = indoc! {"
-        The quick ˇbrowˇnˇ   
+        The quick ˇbrowˇnˇ
         fox ˇjuˇmpsˇ over
-        the lazy dogˇ  
+        the lazy dogˇ
         ˇ
         ˇ
         ˇ
-        Thˇeˇ-ˇquˇickˇ ˇbrownˇ 
-        ˇ  
-        ˇ  
+        Thˇeˇ-ˇquˇickˇ ˇbrownˇ
+        ˇ
+        ˇ
         ˇ  fox-jumpˇs over
-        the lazy dogˇ 
+        the lazy dogˇ
         ˇ
         "};
 
@@ -527,7 +527,7 @@ mod test {
     const SENTENCE_EXAMPLES: &[&'static str] = &[
         "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
         indoc! {"
-            ˇThe quick ˇbrownˇ   
+            ˇThe quick ˇbrownˇ
             fox jumps over
             the lazy doˇgˇ.ˇ ˇThe quick ˇ
             brown fox jumps over

crates/vim/src/test/vim_test_context.rs 🔗

@@ -16,19 +16,15 @@ pub struct VimTestContext<'a> {
 
 impl<'a> VimTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
-        cx.update(|cx| {
-            search::init(cx);
-            crate::init(cx);
-
-            settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
-        });
-
         let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
-
         cx.update(|cx| {
             cx.update_global(|settings: &mut Settings, _| {
                 settings.vim_mode = enabled;
             });
+            search::init(cx);
+            crate::init(cx);
+
+            settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
         });
 
         // Setup search toolbars and keypress hook
@@ -80,7 +76,8 @@ impl<'a> VimTestContext<'a> {
     }
 
     pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
-        self.cx.update(|cx| {
+        let window_id = self.window_id;
+        self.update_window(window_id, |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.switch_mode(mode, false, cx);
             })

crates/vim/src/vim.rs 🔗

@@ -65,7 +65,7 @@ pub fn init(cx: &mut AppContext) {
         // Otherwise forward cancel on to the editor
         let vim = Vim::read(cx);
         if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
-            AppContext::defer(cx, |cx| {
+            WindowContext::defer(cx, |cx| {
                 Vim::update(cx, |state, cx| {
                     state.switch_mode(Mode::Normal, false, cx);
                 });
@@ -83,14 +83,14 @@ pub fn init(cx: &mut AppContext) {
         Vim::active_editor_input_ignored("\n".into(), cx)
     });
 
-    // Sync initial settings with the rest of the app
-    Vim::update(cx, |vim, cx| vim.sync_vim_settings(cx));
-
-    // Any time settings change, update vim mode to match
+    // Any time settings change, update vim mode to match.
+    cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
+        vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+    });
     cx.observe_global::<Settings, _>(|cx| {
-        Vim::update(cx, |state, cx| {
-            state.set_enabled(cx.global::<Settings>().vim_mode, cx)
-        })
+        cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
+            vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+        });
     })
     .detach();
 }
@@ -135,23 +135,23 @@ impl Vim {
         cx.default_global()
     }
 
-    fn update<F, S>(cx: &mut AppContext, update: F) -> S
+    fn update<F, S>(cx: &mut WindowContext, update: F) -> S
     where
-        F: FnOnce(&mut Self, &mut AppContext) -> S,
+        F: FnOnce(&mut Self, &mut WindowContext) -> S,
     {
         cx.update_default_global(update)
     }
 
     fn update_active_editor<S>(
         &self,
-        cx: &mut AppContext,
+        cx: &mut WindowContext,
         update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
     ) -> Option<S> {
         let editor = self.active_editor.clone()?.upgrade(cx)?;
-        cx.update_window(editor.window_id(), |cx| editor.update(cx, update))
+        Some(editor.update(cx, update))
     }
 
-    fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut AppContext) {
+    fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
         self.state.mode = mode;
         self.state.operator_stack.clear();
 
@@ -178,12 +178,12 @@ impl Vim {
         });
     }
 
-    fn push_operator(&mut self, operator: Operator, cx: &mut AppContext) {
+    fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
         self.state.operator_stack.push(operator);
         self.sync_vim_settings(cx);
     }
 
-    fn push_number(&mut self, Number(number): &Number, cx: &mut AppContext) {
+    fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
         if let Some(Operator::Number(current_number)) = self.active_operator() {
             self.pop_operator(cx);
             self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
@@ -192,14 +192,14 @@ impl Vim {
         }
     }
 
-    fn pop_operator(&mut self, cx: &mut AppContext) -> Operator {
+    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");
         self.sync_vim_settings(cx);
         popped_operator
     }
 
-    fn pop_number_operator(&mut self, cx: &mut AppContext) -> usize {
+    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> usize {
         let mut times = 1;
         if let Some(Operator::Number(number)) = self.active_operator() {
             times = number;
@@ -208,7 +208,7 @@ impl Vim {
         times
     }
 
-    fn clear_operator(&mut self, cx: &mut AppContext) {
+    fn clear_operator(&mut self, cx: &mut WindowContext) {
         self.state.operator_stack.clear();
         self.sync_vim_settings(cx);
     }
@@ -217,7 +217,7 @@ impl Vim {
         self.state.operator_stack.last().copied()
     }
 
-    fn active_editor_input_ignored(text: Arc<str>, cx: &mut AppContext) {
+    fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
         if text.is_empty() {
             return;
         }
@@ -242,25 +242,33 @@ impl Vim {
         if self.enabled != enabled {
             self.enabled = enabled;
             self.state = Default::default();
-            if enabled {
-                self.switch_mode(Mode::Normal, false, cx);
-            }
-            self.sync_vim_settings(cx);
+
+            cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
+                if self.enabled {
+                    filter.filtered_namespaces.remove("vim");
+                } else {
+                    filter.filtered_namespaces.insert("vim");
+                }
+            });
+
+            cx.update_active_window(|cx| {
+                if self.enabled {
+                    self.active_editor = cx
+                        .root_view()
+                        .downcast_ref::<Workspace>()
+                        .and_then(|workspace| workspace.read(cx).active_item(cx))
+                        .and_then(|item| item.downcast::<Editor>().map(|h| h.downgrade()));
+                    self.switch_mode(Mode::Normal, false, cx);
+                }
+                self.sync_vim_settings(cx);
+            });
         }
     }
 
-    fn sync_vim_settings(&self, cx: &mut AppContext) {
+    fn sync_vim_settings(&self, cx: &mut WindowContext) {
         let state = &self.state;
         let cursor_shape = state.cursor_shape();
 
-        cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
-            if self.enabled {
-                filter.filtered_namespaces.remove("vim");
-            } else {
-                filter.filtered_namespaces.insert("vim");
-            }
-        });
-
         self.update_active_editor(cx, |editor, cx| {
             if self.enabled && editor.mode() == EditorMode::Full {
                 editor.set_cursor_shape(cursor_shape, cx);

crates/vim/src/visual.rs 🔗

@@ -4,7 +4,7 @@ use collections::HashMap;
 use editor::{
     display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
 };
-use gpui::{actions, AppContext, ViewContext};
+use gpui::{actions, AppContext, ViewContext, WindowContext};
 use language::{AutoindentMode, SelectionGoal};
 use workspace::Workspace;
 
@@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(paste);
 }
 
-pub fn visual_motion(motion: Motion, times: usize, cx: &mut AppContext) {
+pub fn visual_motion(motion: Motion, times: usize, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -56,7 +56,7 @@ pub fn visual_motion(motion: Motion, times: usize, cx: &mut AppContext) {
     });
 }
 
-pub fn visual_object(object: Object, cx: &mut AppContext) {
+pub fn visual_object(object: Object, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if let Operator::Object { around } = vim.pop_operator(cx) {
             vim.update_active_editor(cx, |editor, cx| {
@@ -313,7 +313,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
     });
 }
 
-pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut AppContext) {
+pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {