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, 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, 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                        .repeat_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                    .repeat_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) {
236        if !self.workspace_state.replaying {
237            self.workspace_state.recording = true;
238            self.workspace_state.repeat_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    }
247
248    pub fn stop_recording(&mut self) {
249        if self.workspace_state.recording {
250            self.workspace_state.stop_recording_after_next_action = true;
251        }
252    }
253
254    pub fn record_current_action(&mut self) {
255        self.start_recording();
256        self.stop_recording();
257    }
258
259    fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
260        let state = self.state();
261        let last_mode = state.mode;
262        let prior_mode = state.last_mode;
263        self.update_state(|state| {
264            state.last_mode = last_mode;
265            state.mode = mode;
266            state.operator_stack.clear();
267        });
268
269        cx.emit_global(VimEvent::ModeChanged { mode });
270
271        // Sync editor settings like clip mode
272        self.sync_vim_settings(cx);
273
274        if leave_selections {
275            return;
276        }
277
278        // Adjust selections
279        self.update_active_editor(cx, |editor, cx| {
280            if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
281            {
282                visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
283            }
284
285            editor.change_selections(None, cx, |s| {
286                // we cheat with visual block mode and use multiple cursors.
287                // the cost of this cheat is we need to convert back to a single
288                // cursor whenever vim would.
289                if last_mode == Mode::VisualBlock
290                    && (mode != Mode::VisualBlock && mode != Mode::Insert)
291                {
292                    let tail = s.oldest_anchor().tail();
293                    let head = s.newest_anchor().head();
294                    s.select_anchor_ranges(vec![tail..head]);
295                } else if last_mode == Mode::Insert
296                    && prior_mode == Mode::VisualBlock
297                    && mode != Mode::VisualBlock
298                {
299                    let pos = s.first_anchor().head();
300                    s.select_anchor_ranges(vec![pos..pos])
301                }
302
303                s.move_with(|map, selection| {
304                    if last_mode.is_visual() && !mode.is_visual() {
305                        let mut point = selection.head();
306                        if !selection.reversed && !selection.is_empty() {
307                            point = movement::left(map, selection.head());
308                        }
309                        selection.collapse_to(point, selection.goal)
310                    } else if !last_mode.is_visual() && mode.is_visual() {
311                        if selection.is_empty() {
312                            selection.end = movement::right(map, selection.start);
313                        }
314                    }
315                });
316            })
317        });
318    }
319
320    fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
321        if matches!(
322            operator,
323            Operator::Change | Operator::Delete | Operator::Replace
324        ) {
325            self.start_recording()
326        };
327        self.update_state(|state| state.operator_stack.push(operator));
328        self.sync_vim_settings(cx);
329    }
330
331    fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
332        if let Some(Operator::Number(current_number)) = self.active_operator() {
333            self.pop_operator(cx);
334            self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
335        } else {
336            self.push_operator(Operator::Number(*number as usize), cx);
337        }
338    }
339
340    fn maybe_pop_operator(&mut self) -> Option<Operator> {
341        self.update_state(|state| state.operator_stack.pop())
342    }
343
344    fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
345        let popped_operator = self.update_state( |state| state.operator_stack.pop()
346        )            .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
347        self.sync_vim_settings(cx);
348        popped_operator
349    }
350
351    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
352        if self.workspace_state.replaying {
353            if let Some(number) = self.workspace_state.recorded_count {
354                return Some(number);
355            }
356        }
357
358        if let Some(Operator::Number(number)) = self.active_operator() {
359            self.pop_operator(cx);
360            return Some(number);
361        }
362        None
363    }
364
365    fn clear_operator(&mut self, cx: &mut WindowContext) {
366        self.update_state(|state| state.operator_stack.clear());
367        self.sync_vim_settings(cx);
368    }
369
370    fn active_operator(&self) -> Option<Operator> {
371        self.state().operator_stack.last().copied()
372    }
373
374    fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
375        if text.is_empty() {
376            return;
377        }
378
379        match Vim::read(cx).active_operator() {
380            Some(Operator::FindForward { before }) => {
381                let find = Motion::FindForward { before, text };
382                Vim::update(cx, |vim, _| {
383                    vim.workspace_state.last_find = Some(find.clone())
384                });
385                motion::motion(find, cx)
386            }
387            Some(Operator::FindBackward { after }) => {
388                let find = Motion::FindBackward { after, text };
389                Vim::update(cx, |vim, _| {
390                    vim.workspace_state.last_find = Some(find.clone())
391                });
392                motion::motion(find, cx)
393            }
394            Some(Operator::Replace) => match Vim::read(cx).state().mode {
395                Mode::Normal => normal_replace(text, cx),
396                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
397                _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
398            },
399            _ => {}
400        }
401    }
402
403    fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
404        if self.enabled != enabled {
405            self.enabled = enabled;
406
407            cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
408                if self.enabled {
409                    filter.filtered_namespaces.remove("vim");
410                } else {
411                    filter.filtered_namespaces.insert("vim");
412                }
413            });
414
415            cx.update_active_window(|cx| {
416                if self.enabled {
417                    let active_editor = cx
418                        .root_view()
419                        .downcast_ref::<Workspace>()
420                        .and_then(|workspace| workspace.read(cx).active_item(cx))
421                        .and_then(|item| item.downcast::<Editor>());
422                    if let Some(active_editor) = active_editor {
423                        self.set_active_editor(active_editor, cx);
424                    }
425                    self.switch_mode(Mode::Normal, false, cx);
426                }
427                self.sync_vim_settings(cx);
428            });
429        }
430    }
431
432    pub fn state(&self) -> &EditorState {
433        if let Some(active_editor) = self.active_editor.as_ref() {
434            if let Some(state) = self.editor_states.get(&active_editor.id()) {
435                return state;
436            }
437        }
438
439        &self.default_state
440    }
441
442    pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
443        let mut state = self.state().clone();
444        let ret = func(&mut state);
445
446        if let Some(active_editor) = self.active_editor.as_ref() {
447            self.editor_states.insert(active_editor.id(), state);
448        }
449
450        ret
451    }
452
453    fn sync_vim_settings(&self, cx: &mut WindowContext) {
454        let state = self.state();
455        let cursor_shape = state.cursor_shape();
456
457        self.update_active_editor(cx, |editor, cx| {
458            if self.enabled && editor.mode() == EditorMode::Full {
459                editor.set_cursor_shape(cursor_shape, cx);
460                editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
461                editor.set_collapse_matches(true);
462                editor.set_input_enabled(!state.vim_controlled());
463                editor.set_autoindent(state.should_autoindent());
464                editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
465                let context_layer = state.keymap_context_layer();
466                editor.set_keymap_context_layer::<Self>(context_layer, cx);
467            } else {
468                // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
469                // but we need collapse_matches to persist when the search bar is focused.
470                editor.set_collapse_matches(false);
471                self.unhook_vim_settings(editor, cx);
472            }
473        });
474    }
475
476    fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
477        editor.set_cursor_shape(CursorShape::Bar, cx);
478        editor.set_clip_at_line_ends(false, cx);
479        editor.set_input_enabled(true);
480        editor.set_autoindent(true);
481        editor.selections.line_mode = false;
482
483        // we set the VimEnabled context on all editors so that we
484        // can distinguish between vim mode and non-vim mode in the BufferSearchBar.
485        // This is a bit of a hack, but currently the search crate does not depend on vim,
486        // and it seems nice to keep it that way.
487        if self.enabled {
488            let mut context = KeymapContext::default();
489            context.add_identifier("VimEnabled");
490            editor.set_keymap_context_layer::<Self>(context, cx)
491        } else {
492            editor.remove_keymap_context_layer::<Self>(cx);
493        }
494    }
495}
496
497impl Setting for VimModeSetting {
498    const KEY: Option<&'static str> = Some("vim_mode");
499
500    type FileContent = Option<bool>;
501
502    fn load(
503        default_value: &Self::FileContent,
504        user_values: &[&Self::FileContent],
505        _: &AppContext,
506    ) -> Result<Self> {
507        Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
508            default_value.ok_or_else(Self::missing_default)?,
509        )))
510    }
511}
512
513fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
514    Vim::update(cx, |vim, cx| {
515        if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
516            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
517                vim.switch_mode(Mode::VisualBlock, false, cx);
518            } else {
519                vim.switch_mode(Mode::Visual, false, cx)
520            }
521        }
522    })
523}