vim.rs

  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 {
417                    before,
418                    char: text.chars().next().unwrap(),
419                };
420                Vim::update(cx, |vim, _| {
421                    vim.workspace_state.last_find = Some(find.clone())
422                });
423                motion::motion(find, cx)
424            }
425            Some(Operator::FindBackward { after }) => {
426                let find = Motion::FindBackward {
427                    after,
428                    char: text.chars().next().unwrap(),
429                };
430                Vim::update(cx, |vim, _| {
431                    vim.workspace_state.last_find = Some(find.clone())
432                });
433                motion::motion(find, cx)
434            }
435            Some(Operator::Replace) => match Vim::read(cx).state().mode {
436                Mode::Normal => normal_replace(text, cx),
437                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
438                _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
439            },
440            _ => {}
441        }
442    }
443
444    fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
445        if self.enabled != enabled {
446            self.enabled = enabled;
447
448            cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
449                if self.enabled {
450                    filter.filtered_namespaces.remove("vim");
451                } else {
452                    filter.filtered_namespaces.insert("vim");
453                }
454            });
455
456            cx.update_active_window(|cx| {
457                if self.enabled {
458                    let active_editor = cx
459                        .root_view()
460                        .downcast_ref::<Workspace>()
461                        .and_then(|workspace| workspace.read(cx).active_item(cx))
462                        .and_then(|item| item.downcast::<Editor>());
463                    if let Some(active_editor) = active_editor {
464                        self.set_active_editor(active_editor, cx);
465                    }
466                    self.switch_mode(Mode::Normal, false, cx);
467                }
468                self.sync_vim_settings(cx);
469            });
470        }
471    }
472
473    pub fn state(&self) -> &EditorState {
474        if let Some(active_editor) = self.active_editor.as_ref() {
475            if let Some(state) = self.editor_states.get(&active_editor.id()) {
476                return state;
477            }
478        }
479
480        &self.default_state
481    }
482
483    pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
484        let mut state = self.state().clone();
485        let ret = func(&mut state);
486
487        if let Some(active_editor) = self.active_editor.as_ref() {
488            self.editor_states.insert(active_editor.id(), state);
489        }
490
491        ret
492    }
493
494    fn sync_vim_settings(&self, cx: &mut WindowContext) {
495        let state = self.state();
496        let cursor_shape = state.cursor_shape();
497
498        self.update_active_editor(cx, |editor, cx| {
499            if self.enabled && editor.mode() == EditorMode::Full {
500                editor.set_cursor_shape(cursor_shape, cx);
501                editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
502                editor.set_collapse_matches(true);
503                editor.set_input_enabled(!state.vim_controlled());
504                editor.set_autoindent(state.should_autoindent());
505                editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
506                let context_layer = state.keymap_context_layer();
507                editor.set_keymap_context_layer::<Self>(context_layer, cx);
508            } else {
509                // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
510                // but we need collapse_matches to persist when the search bar is focused.
511                editor.set_collapse_matches(false);
512                self.unhook_vim_settings(editor, cx);
513            }
514        });
515    }
516
517    fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
518        editor.set_cursor_shape(CursorShape::Bar, cx);
519        editor.set_clip_at_line_ends(false, cx);
520        editor.set_input_enabled(true);
521        editor.set_autoindent(true);
522        editor.selections.line_mode = false;
523
524        // we set the VimEnabled context on all editors so that we
525        // can distinguish between vim mode and non-vim mode in the BufferSearchBar.
526        // This is a bit of a hack, but currently the search crate does not depend on vim,
527        // and it seems nice to keep it that way.
528        if self.enabled {
529            let mut context = KeymapContext::default();
530            context.add_identifier("VimEnabled");
531            editor.set_keymap_context_layer::<Self>(context, cx)
532        } else {
533            editor.remove_keymap_context_layer::<Self>(cx);
534        }
535    }
536}
537
538impl Setting for VimModeSetting {
539    const KEY: Option<&'static str> = Some("vim_mode");
540
541    type FileContent = Option<bool>;
542
543    fn load(
544        default_value: &Self::FileContent,
545        user_values: &[&Self::FileContent],
546        _: &AppContext,
547    ) -> Result<Self> {
548        Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
549            default_value.ok_or_else(Self::missing_default)?,
550        )))
551    }
552}
553
554fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
555    Vim::update(cx, |vim, cx| {
556        if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
557            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
558                vim.switch_mode(Mode::VisualBlock, false, cx);
559            } else {
560                vim.switch_mode(Mode::Visual, false, cx)
561            }
562        }
563    })
564}