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