vim.rs

  1#[cfg(test)]
  2mod vim_test_context;
  3
  4mod editor_events;
  5mod insert;
  6mod motion;
  7mod normal;
  8mod state;
  9mod visual;
 10
 11use collections::HashMap;
 12use editor::{CursorShape, Editor};
 13use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
 14use serde::Deserialize;
 15
 16use settings::Settings;
 17use state::{Mode, Operator, VimState};
 18use workspace::{self, Workspace};
 19
 20#[derive(Clone, Deserialize)]
 21pub struct SwitchMode(pub Mode);
 22
 23#[derive(Clone, Deserialize)]
 24pub struct PushOperator(pub Operator);
 25
 26impl_actions!(vim, [SwitchMode, PushOperator]);
 27
 28pub fn init(cx: &mut MutableAppContext) {
 29    editor_events::init(cx);
 30    normal::init(cx);
 31    visual::init(cx);
 32    insert::init(cx);
 33    motion::init(cx);
 34
 35    cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
 36        Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
 37    });
 38    cx.add_action(
 39        |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
 40            Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
 41        },
 42    );
 43
 44    cx.observe_global::<Settings, _>(|settings, cx| {
 45        Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
 46    })
 47    .detach();
 48}
 49
 50#[derive(Default)]
 51pub struct Vim {
 52    editors: HashMap<usize, WeakViewHandle<Editor>>,
 53    active_editor: Option<WeakViewHandle<Editor>>,
 54
 55    enabled: bool,
 56    state: VimState,
 57}
 58
 59impl Vim {
 60    fn read(cx: &mut MutableAppContext) -> &Self {
 61        cx.default_global()
 62    }
 63
 64    fn update<F, S>(cx: &mut MutableAppContext, update: F) -> S
 65    where
 66        F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
 67    {
 68        cx.update_default_global(update)
 69    }
 70
 71    fn update_active_editor<S>(
 72        &self,
 73        cx: &mut MutableAppContext,
 74        update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
 75    ) -> Option<S> {
 76        self.active_editor
 77            .clone()
 78            .and_then(|ae| ae.upgrade(cx))
 79            .map(|ae| ae.update(cx, update))
 80    }
 81
 82    fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
 83        self.state.mode = mode;
 84        self.state.operator_stack.clear();
 85        self.sync_editor_options(cx);
 86    }
 87
 88    fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
 89        self.state.operator_stack.push(operator);
 90        self.sync_editor_options(cx);
 91    }
 92
 93    fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
 94        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");
 95        self.sync_editor_options(cx);
 96        popped_operator
 97    }
 98
 99    fn clear_operator(&mut self, cx: &mut MutableAppContext) {
100        self.state.operator_stack.clear();
101        self.sync_editor_options(cx);
102    }
103
104    fn active_operator(&mut self) -> Option<Operator> {
105        self.state.operator_stack.last().copied()
106    }
107
108    fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
109        if self.enabled != enabled {
110            self.enabled = enabled;
111            self.state = Default::default();
112            if enabled {
113                self.state.mode = Mode::Normal;
114            }
115            self.sync_editor_options(cx);
116        }
117    }
118
119    fn sync_editor_options(&self, cx: &mut MutableAppContext) {
120        let state = &self.state;
121
122        let cursor_shape = state.cursor_shape();
123        for editor in self.editors.values() {
124            if let Some(editor) = editor.upgrade(cx) {
125                editor.update(cx, |editor, cx| {
126                    if self.enabled {
127                        editor.set_cursor_shape(cursor_shape, cx);
128                        editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
129                        editor.set_input_enabled(!state.vim_controlled());
130                        let context_layer = state.keymap_context_layer();
131                        editor.set_keymap_context_layer::<Self>(context_layer);
132                    } else {
133                        editor.set_cursor_shape(CursorShape::Bar, cx);
134                        editor.set_clip_at_line_ends(false, cx);
135                        editor.set_input_enabled(true);
136                        editor.remove_keymap_context_layer::<Self>();
137                    }
138                });
139            }
140        }
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use crate::{state::Mode, vim_test_context::VimTestContext};
147
148    #[gpui::test]
149    async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
150        let mut cx = VimTestContext::new(cx, false).await;
151        cx.simulate_keystrokes(["h", "j", "k", "l"]);
152        cx.assert_editor_state("hjkl|");
153    }
154
155    #[gpui::test]
156    async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
157        let mut cx = VimTestContext::new(cx, true).await;
158
159        cx.simulate_keystroke("i");
160        assert_eq!(cx.mode(), Mode::Insert);
161
162        // Editor acts as though vim is disabled
163        cx.disable_vim();
164        cx.simulate_keystrokes(["h", "j", "k", "l"]);
165        cx.assert_editor_state("hjkl|");
166
167        // Enabling dynamically sets vim mode again and restores normal mode
168        cx.enable_vim();
169        assert_eq!(cx.mode(), Mode::Normal);
170        cx.simulate_keystrokes(["h", "h", "h", "l"]);
171        assert_eq!(cx.editor_text(), "hjkl".to_owned());
172        cx.assert_editor_state("hj|kl");
173        cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
174        cx.assert_editor_state("hjTest|kl");
175
176        // Disabling and enabling resets to normal mode
177        assert_eq!(cx.mode(), Mode::Insert);
178        cx.disable_vim();
179        cx.enable_vim();
180        assert_eq!(cx.mode(), Mode::Normal);
181    }
182}