1#[cfg(test)]
2mod test;
3
4mod editor_events;
5mod insert;
6mod mode_indicator;
7mod motion;
8mod normal;
9mod object;
10mod state;
11mod utils;
12mod visual;
13
14use anyhow::Result;
15use collections::{CommandPaletteFilter, HashMap};
16use editor::{movement, Editor, EditorMode, Event};
17use gpui::{
18 actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
19 Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
20};
21use language::{CursorShape, Point, Selection, SelectionGoal};
22pub use mode_indicator::ModeIndicator;
23use motion::Motion;
24use normal::normal_replace;
25use serde::Deserialize;
26use settings::{Setting, SettingsStore};
27use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
28use std::{ops::Range, sync::Arc};
29use visual::{visual_block_motion, visual_replace};
30use workspace::{self, Workspace};
31
32use crate::state::ReplayableAction;
33
34struct VimModeSetting(bool);
35
36#[derive(Clone, Deserialize, PartialEq)]
37pub struct SwitchMode(pub Mode);
38
39#[derive(Clone, Deserialize, PartialEq)]
40pub struct PushOperator(pub Operator);
41
42#[derive(Clone, Deserialize, PartialEq)]
43struct Number(u8);
44
45actions!(vim, [Tab, Enter]);
46impl_actions!(vim, [Number, SwitchMode, PushOperator]);
47
48#[derive(Copy, Clone, Debug)]
49enum VimEvent {
50 ModeChanged { mode: Mode },
51}
52
53pub fn init(cx: &mut AppContext) {
54 settings::register::<VimModeSetting>(cx);
55
56 editor_events::init(cx);
57 normal::init(cx);
58 visual::init(cx);
59 insert::init(cx);
60 object::init(cx);
61 motion::init(cx);
62
63 // Vim Actions
64 cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
65 Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
66 });
67 cx.add_action(
68 |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
69 Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
70 },
71 );
72 cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
73 Vim::update(cx, |vim, cx| vim.push_number(n, cx));
74 });
75
76 cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
77 Vim::active_editor_input_ignored(" ".into(), cx)
78 });
79
80 cx.add_action(|_: &mut Workspace, _: &Enter, cx| {
81 Vim::active_editor_input_ignored("\n".into(), cx)
82 });
83
84 // Any time settings change, update vim mode to match. The Vim struct
85 // will be initialized as disabled by default, so we filter its commands
86 // out when starting up.
87 cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
88 filter.filtered_namespaces.insert("vim");
89 });
90 cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
91 vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
92 });
93 cx.observe_global::<SettingsStore, _>(|cx| {
94 cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
95 vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
96 });
97 })
98 .detach();
99}
100
101pub fn observe_keystrokes(cx: &mut WindowContext) {
102 cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
103 if result == &MatchResult::Pending {
104 return true;
105 }
106 if let Some(handled_by) = handled_by {
107 Vim::update(cx, |vim, _| {
108 if vim.workspace_state.recording {
109 vim.workspace_state
110 .recorded_actions
111 .push(ReplayableAction::Action(handled_by.boxed_clone()));
112
113 if vim.workspace_state.stop_recording_after_next_action {
114 vim.workspace_state.recording = false;
115 vim.workspace_state.stop_recording_after_next_action = false;
116 }
117 }
118 });
119
120 // Keystroke is handled by the vim system, so continue forward
121 if handled_by.namespace() == "vim" {
122 return true;
123 }
124 }
125
126 Vim::update(cx, |vim, cx| match vim.active_operator() {
127 Some(
128 Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace,
129 ) => {}
130 Some(_) => {
131 vim.clear_operator(cx);
132 }
133 _ => {}
134 });
135 true
136 })
137 .detach()
138}
139
140#[derive(Default)]
141pub struct Vim {
142 active_editor: Option<WeakViewHandle<Editor>>,
143 editor_subscription: Option<Subscription>,
144 enabled: bool,
145 editor_states: HashMap<usize, EditorState>,
146 workspace_state: WorkspaceState,
147 default_state: EditorState,
148}
149
150impl Vim {
151 fn read(cx: &mut AppContext) -> &Self {
152 cx.default_global()
153 }
154
155 fn update<F, S>(cx: &mut WindowContext, update: F) -> S
156 where
157 F: FnOnce(&mut Self, &mut WindowContext) -> S,
158 {
159 cx.update_default_global(update)
160 }
161
162 fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) {
163 self.active_editor = Some(editor.clone().downgrade());
164 self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
165 Event::SelectionsChanged { local: true } => {
166 let editor = editor.read(cx);
167 if editor.leader_replica_id().is_none() {
168 let newest = editor.selections.newest::<usize>(cx);
169 local_selections_changed(newest, cx);
170 }
171 }
172 Event::InputIgnored { text } => {
173 Vim::active_editor_input_ignored(text.clone(), cx);
174 Vim::record_insertion(text, None, cx)
175 }
176 Event::InputHandled {
177 text,
178 utf16_range_to_replace: range_to_replace,
179 } => Vim::record_insertion(text, range_to_replace.clone(), cx),
180 _ => {}
181 }));
182
183 if self.enabled {
184 let editor = editor.read(cx);
185 let editor_mode = editor.mode();
186 let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
187
188 if editor_mode == EditorMode::Full
189 && !newest_selection_empty
190 && self.state().mode == Mode::Normal
191 {
192 self.switch_mode(Mode::Visual, true, cx);
193 }
194 }
195
196 self.sync_vim_settings(cx);
197 }
198
199 fn record_insertion(
200 text: &Arc<str>,
201 range_to_replace: Option<Range<isize>>,
202 cx: &mut WindowContext,
203 ) {
204 Vim::update(cx, |vim, _| {
205 if vim.workspace_state.recording {
206 vim.workspace_state
207 .recorded_actions
208 .push(ReplayableAction::Insertion {
209 text: text.clone(),
210 utf16_range_to_replace: range_to_replace,
211 });
212 if vim.workspace_state.stop_recording_after_next_action {
213 vim.workspace_state.recording = false;
214 vim.workspace_state.stop_recording_after_next_action = false;
215 }
216 }
217 });
218 }
219
220 fn update_active_editor<S>(
221 &self,
222 cx: &mut WindowContext,
223 update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
224 ) -> Option<S> {
225 let editor = self.active_editor.clone()?.upgrade(cx)?;
226 Some(editor.update(cx, update))
227 }
228 // ~, shift-j, x, shift-x, p
229 // shift-c, shift-d, shift-i, i, a, o, shift-o, s
230 // c, d
231 // r
232
233 // TODO: shift-j?
234 //
235 pub fn start_recording(&mut self, cx: &mut WindowContext) {
236 if !self.workspace_state.replaying {
237 self.workspace_state.recording = true;
238 self.workspace_state.recorded_actions = Default::default();
239 self.workspace_state.recorded_count =
240 if let Some(Operator::Number(number)) = self.active_operator() {
241 Some(number)
242 } else {
243 None
244 };
245
246 let selections = self
247 .active_editor
248 .and_then(|editor| editor.upgrade(cx))
249 .map(|editor| {
250 let editor = editor.read(cx);
251 (
252 editor.selections.oldest::<Point>(cx),
253 editor.selections.newest::<Point>(cx),
254 )
255 });
256
257 if let Some((oldest, newest)) = selections {
258 self.workspace_state.recorded_selection = match self.state().mode {
259 Mode::Visual if newest.end.row == newest.start.row => {
260 RecordedSelection::SingleLine {
261 cols: newest.end.column - newest.start.column,
262 }
263 }
264 Mode::Visual => RecordedSelection::Visual {
265 rows: newest.end.row - newest.start.row,
266 cols: newest.end.column,
267 },
268 Mode::VisualLine => RecordedSelection::VisualLine {
269 rows: newest.end.row - newest.start.row,
270 },
271 Mode::VisualBlock => RecordedSelection::VisualBlock {
272 rows: newest.end.row.abs_diff(oldest.start.row),
273 cols: newest.end.column.abs_diff(oldest.start.column),
274 },
275 _ => RecordedSelection::None,
276 }
277 } else {
278 self.workspace_state.recorded_selection = RecordedSelection::None;
279 }
280 }
281 }
282
283 pub fn stop_recording(&mut self) {
284 if self.workspace_state.recording {
285 self.workspace_state.stop_recording_after_next_action = true;
286 }
287 }
288
289 pub fn record_current_action(&mut self, cx: &mut WindowContext) {
290 self.start_recording(cx);
291 self.stop_recording();
292 }
293
294 fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
295 let state = self.state();
296 let last_mode = state.mode;
297 let prior_mode = state.last_mode;
298 self.update_state(|state| {
299 state.last_mode = last_mode;
300 state.mode = mode;
301 state.operator_stack.clear();
302 });
303
304 cx.emit_global(VimEvent::ModeChanged { mode });
305
306 // Sync editor settings like clip mode
307 self.sync_vim_settings(cx);
308
309 if leave_selections {
310 return;
311 }
312
313 // Adjust selections
314 self.update_active_editor(cx, |editor, cx| {
315 if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
316 {
317 visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
318 }
319
320 editor.change_selections(None, cx, |s| {
321 // we cheat with visual block mode and use multiple cursors.
322 // the cost of this cheat is we need to convert back to a single
323 // cursor whenever vim would.
324 if last_mode == Mode::VisualBlock
325 && (mode != Mode::VisualBlock && mode != Mode::Insert)
326 {
327 let tail = s.oldest_anchor().tail();
328 let head = s.newest_anchor().head();
329 s.select_anchor_ranges(vec![tail..head]);
330 } else if last_mode == Mode::Insert
331 && prior_mode == Mode::VisualBlock
332 && mode != Mode::VisualBlock
333 {
334 let pos = s.first_anchor().head();
335 s.select_anchor_ranges(vec![pos..pos])
336 }
337
338 s.move_with(|map, selection| {
339 if last_mode.is_visual() && !mode.is_visual() {
340 let mut point = selection.head();
341 if !selection.reversed && !selection.is_empty() {
342 point = movement::left(map, selection.head());
343 }
344 selection.collapse_to(point, selection.goal)
345 } else if !last_mode.is_visual() && mode.is_visual() {
346 if selection.is_empty() {
347 selection.end = movement::right(map, selection.start);
348 }
349 }
350 });
351 })
352 });
353 }
354
355 fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
356 if matches!(
357 operator,
358 Operator::Change | Operator::Delete | Operator::Replace
359 ) {
360 self.start_recording(cx)
361 };
362 self.update_state(|state| state.operator_stack.push(operator));
363 self.sync_vim_settings(cx);
364 }
365
366 fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
367 if let Some(Operator::Number(current_number)) = self.active_operator() {
368 self.pop_operator(cx);
369 self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
370 } else {
371 self.push_operator(Operator::Number(*number as usize), cx);
372 }
373 }
374
375 fn maybe_pop_operator(&mut self) -> Option<Operator> {
376 self.update_state(|state| state.operator_stack.pop())
377 }
378
379 fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
380 let popped_operator = self.update_state( |state| state.operator_stack.pop()
381 ) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
382 self.sync_vim_settings(cx);
383 popped_operator
384 }
385
386 fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
387 if self.workspace_state.replaying {
388 if let Some(number) = self.workspace_state.recorded_count {
389 return Some(number);
390 }
391 }
392
393 if let Some(Operator::Number(number)) = self.active_operator() {
394 self.pop_operator(cx);
395 return Some(number);
396 }
397 None
398 }
399
400 fn clear_operator(&mut self, cx: &mut WindowContext) {
401 self.update_state(|state| state.operator_stack.clear());
402 self.sync_vim_settings(cx);
403 }
404
405 fn active_operator(&self) -> Option<Operator> {
406 self.state().operator_stack.last().copied()
407 }
408
409 fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
410 if text.is_empty() {
411 return;
412 }
413
414 match Vim::read(cx).active_operator() {
415 Some(Operator::FindForward { before }) => {
416 let find = Motion::FindForward { before, text };
417 Vim::update(cx, |vim, _| {
418 vim.workspace_state.last_find = Some(find.clone())
419 });
420 motion::motion(find, cx)
421 }
422 Some(Operator::FindBackward { after }) => {
423 let find = Motion::FindBackward { after, text };
424 Vim::update(cx, |vim, _| {
425 vim.workspace_state.last_find = Some(find.clone())
426 });
427 motion::motion(find, cx)
428 }
429 Some(Operator::Replace) => match Vim::read(cx).state().mode {
430 Mode::Normal => normal_replace(text, cx),
431 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
432 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
433 },
434 _ => {}
435 }
436 }
437
438 fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
439 if self.enabled != enabled {
440 self.enabled = enabled;
441
442 cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
443 if self.enabled {
444 filter.filtered_namespaces.remove("vim");
445 } else {
446 filter.filtered_namespaces.insert("vim");
447 }
448 });
449
450 cx.update_active_window(|cx| {
451 if self.enabled {
452 let active_editor = cx
453 .root_view()
454 .downcast_ref::<Workspace>()
455 .and_then(|workspace| workspace.read(cx).active_item(cx))
456 .and_then(|item| item.downcast::<Editor>());
457 if let Some(active_editor) = active_editor {
458 self.set_active_editor(active_editor, cx);
459 }
460 self.switch_mode(Mode::Normal, false, cx);
461 }
462 self.sync_vim_settings(cx);
463 });
464 }
465 }
466
467 pub fn state(&self) -> &EditorState {
468 if let Some(active_editor) = self.active_editor.as_ref() {
469 if let Some(state) = self.editor_states.get(&active_editor.id()) {
470 return state;
471 }
472 }
473
474 &self.default_state
475 }
476
477 pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
478 let mut state = self.state().clone();
479 let ret = func(&mut state);
480
481 if let Some(active_editor) = self.active_editor.as_ref() {
482 self.editor_states.insert(active_editor.id(), state);
483 }
484
485 ret
486 }
487
488 fn sync_vim_settings(&self, cx: &mut WindowContext) {
489 let state = self.state();
490 let cursor_shape = state.cursor_shape();
491
492 self.update_active_editor(cx, |editor, cx| {
493 if self.enabled && editor.mode() == EditorMode::Full {
494 editor.set_cursor_shape(cursor_shape, cx);
495 editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
496 editor.set_collapse_matches(true);
497 editor.set_input_enabled(!state.vim_controlled());
498 editor.set_autoindent(state.should_autoindent());
499 editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
500 let context_layer = state.keymap_context_layer();
501 editor.set_keymap_context_layer::<Self>(context_layer, cx);
502 } else {
503 // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
504 // but we need collapse_matches to persist when the search bar is focused.
505 editor.set_collapse_matches(false);
506 self.unhook_vim_settings(editor, cx);
507 }
508 });
509 }
510
511 fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
512 editor.set_cursor_shape(CursorShape::Bar, cx);
513 editor.set_clip_at_line_ends(false, cx);
514 editor.set_input_enabled(true);
515 editor.set_autoindent(true);
516 editor.selections.line_mode = false;
517
518 // we set the VimEnabled context on all editors so that we
519 // can distinguish between vim mode and non-vim mode in the BufferSearchBar.
520 // This is a bit of a hack, but currently the search crate does not depend on vim,
521 // and it seems nice to keep it that way.
522 if self.enabled {
523 let mut context = KeymapContext::default();
524 context.add_identifier("VimEnabled");
525 editor.set_keymap_context_layer::<Self>(context, cx)
526 } else {
527 editor.remove_keymap_context_layer::<Self>(cx);
528 }
529 }
530}
531
532impl Setting for VimModeSetting {
533 const KEY: Option<&'static str> = Some("vim_mode");
534
535 type FileContent = Option<bool>;
536
537 fn load(
538 default_value: &Self::FileContent,
539 user_values: &[&Self::FileContent],
540 _: &AppContext,
541 ) -> Result<Self> {
542 Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
543 default_value.ok_or_else(Self::missing_default)?,
544 )))
545 }
546}
547
548fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
549 Vim::update(cx, |vim, cx| {
550 if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
551 if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
552 vim.switch_mode(Mode::VisualBlock, false, cx);
553 } else {
554 vim.switch_mode(Mode::Visual, false, cx)
555 }
556 }
557 })
558}