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