vim.rs

  1//! Vim support for Zed.
  2
  3#[cfg(test)]
  4mod test;
  5
  6mod command;
  7mod editor_events;
  8mod insert;
  9mod mode_indicator;
 10mod motion;
 11mod normal;
 12mod object;
 13mod state;
 14mod utils;
 15mod visual;
 16
 17use anyhow::Result;
 18use collections::HashMap;
 19use command_palette::CommandPaletteInterceptor;
 20use copilot::CommandPaletteFilter;
 21use editor::{movement, Editor, EditorEvent, EditorMode};
 22use gpui::{
 23    actions, impl_actions, Action, AppContext, EntityId, Global, Subscription, View, ViewContext,
 24    WeakView, WindowContext,
 25};
 26use language::{CursorShape, Point, Selection, SelectionGoal};
 27pub use mode_indicator::ModeIndicator;
 28use motion::Motion;
 29use normal::normal_replace;
 30use schemars::JsonSchema;
 31use serde::Deserialize;
 32use serde_derive::Serialize;
 33use settings::{update_settings_file, Settings, SettingsStore};
 34use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 35use std::{ops::Range, sync::Arc};
 36use visual::{visual_block_motion, visual_replace};
 37use workspace::{self, Workspace};
 38
 39use crate::state::ReplayableAction;
 40
 41/// Whether or not to enable Vim mode (work in progress).
 42///
 43/// Default: false
 44pub struct VimModeSetting(pub bool);
 45
 46/// An Action to Switch between modes
 47#[derive(Clone, Deserialize, PartialEq)]
 48pub struct SwitchMode(pub Mode);
 49
 50/// PushOperator is used to put vim into a "minor" mode,
 51/// where it's waiting for a specific next set of keystrokes.
 52/// For example 'd' needs a motion to complete.
 53#[derive(Clone, Deserialize, PartialEq)]
 54pub struct PushOperator(pub Operator);
 55
 56/// Number is used to manage vim's count. Pushing a digit
 57/// multiplis the current value by 10 and adds the digit.
 58#[derive(Clone, Deserialize, PartialEq)]
 59struct Number(usize);
 60
 61actions!(
 62    vim,
 63    [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
 64);
 65
 66// in the workspace namespace so it's not filtered out when vim is disabled.
 67actions!(workspace, [ToggleVimMode]);
 68
 69impl_actions!(vim, [SwitchMode, PushOperator, Number]);
 70
 71/// Initializes the `vim` crate.
 72pub fn init(cx: &mut AppContext) {
 73    cx.set_global(Vim::default());
 74    VimModeSetting::register(cx);
 75    VimSettings::register(cx);
 76
 77    editor_events::init(cx);
 78
 79    cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx))
 80        .detach();
 81
 82    // Any time settings change, update vim mode to match. The Vim struct
 83    // will be initialized as disabled by default, so we filter its commands
 84    // out when starting up.
 85    cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
 86        filter.hidden_namespaces.insert("vim");
 87    });
 88    cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
 89        vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
 90    });
 91    cx.observe_global::<SettingsStore>(|cx| {
 92        cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
 93            vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
 94        });
 95    })
 96    .detach();
 97}
 98
 99fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
100    workspace.register_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
101        Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
102    });
103    workspace.register_action(
104        |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
105            Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
106        },
107    );
108    workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| {
109        Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
110    });
111
112    workspace.register_action(|_: &mut Workspace, _: &Tab, cx| {
113        Vim::active_editor_input_ignored(" ".into(), cx)
114    });
115
116    workspace.register_action(|_: &mut Workspace, _: &Enter, cx| {
117        Vim::active_editor_input_ignored("\n".into(), cx)
118    });
119
120    workspace.register_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
121        let fs = workspace.app_state().fs.clone();
122        let currently_enabled = VimModeSetting::get_global(cx).0;
123        update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
124            *setting = Some(!currently_enabled)
125        })
126    });
127
128    normal::register(workspace, cx);
129    insert::register(workspace, cx);
130    motion::register(workspace, cx);
131    command::register(workspace, cx);
132    object::register(workspace, cx);
133    visual::register(workspace, cx);
134}
135
136/// Registers a keystroke observer to observe keystrokes for the Vim integration.
137pub fn observe_keystrokes(cx: &mut WindowContext) {
138    cx.observe_keystrokes(|keystroke_event, cx| {
139        if let Some(action) = keystroke_event
140            .action
141            .as_ref()
142            .map(|action| action.boxed_clone())
143        {
144            Vim::update(cx, |vim, _| {
145                if vim.workspace_state.recording {
146                    vim.workspace_state
147                        .recorded_actions
148                        .push(ReplayableAction::Action(action.boxed_clone()));
149
150                    if vim.workspace_state.stop_recording_after_next_action {
151                        vim.workspace_state.recording = false;
152                        vim.workspace_state.stop_recording_after_next_action = false;
153                    }
154                }
155            });
156
157            // Keystroke is handled by the vim system, so continue forward
158            if action.name().starts_with("vim::") {
159                return;
160            }
161        } else if cx.has_pending_keystrokes() {
162            return;
163        }
164
165        Vim::update(cx, |vim, cx| match vim.active_operator() {
166            Some(
167                Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace,
168            ) => {}
169            Some(_) => {
170                vim.clear_operator(cx);
171            }
172            _ => {}
173        });
174    })
175    .detach()
176}
177
178/// The state pertaining to Vim mode.
179#[derive(Default)]
180struct Vim {
181    active_editor: Option<WeakView<Editor>>,
182    editor_subscription: Option<Subscription>,
183    enabled: bool,
184    editor_states: HashMap<EntityId, EditorState>,
185    workspace_state: WorkspaceState,
186    default_state: EditorState,
187}
188
189impl Global for Vim {}
190
191impl Vim {
192    fn read(cx: &mut AppContext) -> &Self {
193        cx.global::<Self>()
194    }
195
196    fn update<F, S>(cx: &mut WindowContext, update: F) -> S
197    where
198        F: FnOnce(&mut Self, &mut WindowContext) -> S,
199    {
200        cx.update_global(update)
201    }
202
203    fn activate_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
204        if !editor.read(cx).use_modal_editing() {
205            return;
206        }
207
208        self.active_editor = Some(editor.clone().downgrade());
209        self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
210            EditorEvent::SelectionsChanged { local: true } => {
211                let editor = editor.read(cx);
212                if editor.leader_peer_id().is_none() {
213                    let newest = editor.selections.newest::<usize>(cx);
214                    let is_multicursor = editor.selections.count() > 1;
215                    local_selections_changed(newest, is_multicursor, cx);
216                }
217            }
218            EditorEvent::InputIgnored { text } => {
219                Vim::active_editor_input_ignored(text.clone(), cx);
220                Vim::record_insertion(text, None, cx)
221            }
222            EditorEvent::InputHandled {
223                text,
224                utf16_range_to_replace: range_to_replace,
225            } => Vim::record_insertion(text, range_to_replace.clone(), cx),
226            _ => {}
227        }));
228
229        let editor = editor.read(cx);
230        let editor_mode = editor.mode();
231        let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
232
233        if editor_mode == EditorMode::Full
234                && !newest_selection_empty
235                && self.state().mode == Mode::Normal
236                // When following someone, don't switch vim mode.
237                && editor.leader_peer_id().is_none()
238        {
239            self.switch_mode(Mode::Visual, true, cx);
240        }
241
242        self.sync_vim_settings(cx);
243    }
244
245    fn record_insertion(
246        text: &Arc<str>,
247        range_to_replace: Option<Range<isize>>,
248        cx: &mut WindowContext,
249    ) {
250        Vim::update(cx, |vim, _| {
251            if vim.workspace_state.recording {
252                vim.workspace_state
253                    .recorded_actions
254                    .push(ReplayableAction::Insertion {
255                        text: text.clone(),
256                        utf16_range_to_replace: range_to_replace,
257                    });
258                if vim.workspace_state.stop_recording_after_next_action {
259                    vim.workspace_state.recording = false;
260                    vim.workspace_state.stop_recording_after_next_action = false;
261                }
262            }
263        });
264    }
265
266    fn update_active_editor<S>(
267        &mut self,
268        cx: &mut WindowContext,
269        update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext<Editor>) -> S,
270    ) -> Option<S> {
271        let editor = self.active_editor.clone()?.upgrade()?;
272        Some(editor.update(cx, |editor, cx| update(self, editor, cx)))
273    }
274
275    /// When doing an action that modifies the buffer, we start recording so that `.`
276    /// will replay the action.
277    pub fn start_recording(&mut self, cx: &mut WindowContext) {
278        if !self.workspace_state.replaying {
279            self.workspace_state.recording = true;
280            self.workspace_state.recorded_actions = Default::default();
281            self.workspace_state.recorded_count = None;
282
283            let selections = self
284                .active_editor
285                .as_ref()
286                .and_then(|editor| editor.upgrade())
287                .map(|editor| {
288                    let editor = editor.read(cx);
289                    (
290                        editor.selections.oldest::<Point>(cx),
291                        editor.selections.newest::<Point>(cx),
292                    )
293                });
294
295            if let Some((oldest, newest)) = selections {
296                self.workspace_state.recorded_selection = match self.state().mode {
297                    Mode::Visual if newest.end.row == newest.start.row => {
298                        RecordedSelection::SingleLine {
299                            cols: newest.end.column - newest.start.column,
300                        }
301                    }
302                    Mode::Visual => RecordedSelection::Visual {
303                        rows: newest.end.row - newest.start.row,
304                        cols: newest.end.column,
305                    },
306                    Mode::VisualLine => RecordedSelection::VisualLine {
307                        rows: newest.end.row - newest.start.row,
308                    },
309                    Mode::VisualBlock => RecordedSelection::VisualBlock {
310                        rows: newest.end.row.abs_diff(oldest.start.row),
311                        cols: newest.end.column.abs_diff(oldest.start.column),
312                    },
313                    _ => RecordedSelection::None,
314                }
315            } else {
316                self.workspace_state.recorded_selection = RecordedSelection::None;
317            }
318        }
319    }
320
321    /// When finishing an action that modifies the buffer, stop recording.
322    /// as you usually call this within a keystroke handler we also ensure that
323    /// the current action is recorded.
324    pub fn stop_recording(&mut self) {
325        if self.workspace_state.recording {
326            self.workspace_state.stop_recording_after_next_action = true;
327        }
328    }
329
330    /// Stops recording actions immediately rather than waiting until after the
331    /// next action to stop recording.
332    ///
333    /// This doesn't include the current action.
334    pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
335        if self.workspace_state.recording {
336            self.workspace_state
337                .recorded_actions
338                .push(ReplayableAction::Action(action.boxed_clone()));
339            self.workspace_state.recording = false;
340            self.workspace_state.stop_recording_after_next_action = false;
341        }
342    }
343
344    /// Explicitly record one action (equivalents to start_recording and stop_recording)
345    pub fn record_current_action(&mut self, cx: &mut WindowContext) {
346        self.start_recording(cx);
347        self.stop_recording();
348    }
349
350    fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
351        let state = self.state();
352        let last_mode = state.mode;
353        let prior_mode = state.last_mode;
354        self.update_state(|state| {
355            state.last_mode = last_mode;
356            state.mode = mode;
357            state.operator_stack.clear();
358        });
359        if mode != Mode::Insert {
360            self.take_count(cx);
361        }
362
363        // Sync editor settings like clip mode
364        self.sync_vim_settings(cx);
365
366        if leave_selections {
367            return;
368        }
369
370        // Adjust selections
371        self.update_active_editor(cx, |_, editor, cx| {
372            if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
373            {
374                visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
375            }
376
377            editor.change_selections(None, cx, |s| {
378                // we cheat with visual block mode and use multiple cursors.
379                // the cost of this cheat is we need to convert back to a single
380                // cursor whenever vim would.
381                if last_mode == Mode::VisualBlock
382                    && (mode != Mode::VisualBlock && mode != Mode::Insert)
383                {
384                    let tail = s.oldest_anchor().tail();
385                    let head = s.newest_anchor().head();
386                    s.select_anchor_ranges(vec![tail..head]);
387                } else if last_mode == Mode::Insert
388                    && prior_mode == Mode::VisualBlock
389                    && mode != Mode::VisualBlock
390                {
391                    let pos = s.first_anchor().head();
392                    s.select_anchor_ranges(vec![pos..pos])
393                }
394
395                s.move_with(|map, selection| {
396                    if last_mode.is_visual() && !mode.is_visual() {
397                        let mut point = selection.head();
398                        if !selection.reversed && !selection.is_empty() {
399                            point = movement::left(map, selection.head());
400                        }
401                        selection.collapse_to(point, selection.goal)
402                    } else if !last_mode.is_visual() && mode.is_visual() {
403                        if selection.is_empty() {
404                            selection.end = movement::right(map, selection.start);
405                        }
406                    }
407                });
408            })
409        });
410    }
411
412    fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
413        if self.active_operator().is_some() {
414            self.update_state(|state| {
415                state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
416            })
417        } else {
418            self.update_state(|state| {
419                state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
420            })
421        }
422        // update the keymap so that 0 works
423        self.sync_vim_settings(cx)
424    }
425
426    fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
427        if self.workspace_state.replaying {
428            return self.workspace_state.recorded_count;
429        }
430
431        let count = if self.state().post_count == None && self.state().pre_count == None {
432            return None;
433        } else {
434            Some(self.update_state(|state| {
435                state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
436            }))
437        };
438        if self.workspace_state.recording {
439            self.workspace_state.recorded_count = count;
440        }
441        self.sync_vim_settings(cx);
442        count
443    }
444
445    fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
446        if matches!(
447            operator,
448            Operator::Change | Operator::Delete | Operator::Replace
449        ) {
450            self.start_recording(cx)
451        };
452        self.update_state(|state| state.operator_stack.push(operator));
453        self.sync_vim_settings(cx);
454    }
455
456    fn maybe_pop_operator(&mut self) -> Option<Operator> {
457        self.update_state(|state| state.operator_stack.pop())
458    }
459
460    fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
461        let popped_operator = self.update_state( |state| state.operator_stack.pop()
462        )            .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
463        self.sync_vim_settings(cx);
464        popped_operator
465    }
466    fn clear_operator(&mut self, cx: &mut WindowContext) {
467        self.take_count(cx);
468        self.update_state(|state| state.operator_stack.clear());
469        self.sync_vim_settings(cx);
470    }
471
472    fn active_operator(&self) -> Option<Operator> {
473        self.state().operator_stack.last().copied()
474    }
475
476    fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
477        if text.is_empty() {
478            return;
479        }
480
481        match Vim::read(cx).active_operator() {
482            Some(Operator::FindForward { before }) => {
483                let find = Motion::FindForward {
484                    before,
485                    char: text.chars().next().unwrap(),
486                };
487                Vim::update(cx, |vim, _| {
488                    vim.workspace_state.last_find = Some(find.clone())
489                });
490                motion::motion(find, cx)
491            }
492            Some(Operator::FindBackward { after }) => {
493                let find = Motion::FindBackward {
494                    after,
495                    char: text.chars().next().unwrap(),
496                };
497                Vim::update(cx, |vim, _| {
498                    vim.workspace_state.last_find = Some(find.clone())
499                });
500                motion::motion(find, cx)
501            }
502            Some(Operator::Replace) => match Vim::read(cx).state().mode {
503                Mode::Normal => normal_replace(text, cx),
504                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
505                _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
506            },
507            _ => {}
508        }
509    }
510
511    fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
512        if self.enabled == enabled {
513            return;
514        }
515        if !enabled {
516            let _ = cx.remove_global::<CommandPaletteInterceptor>();
517            cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
518                filter.hidden_namespaces.insert("vim");
519            });
520            *self = Default::default();
521            return;
522        }
523
524        self.enabled = true;
525        cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
526            filter.hidden_namespaces.remove("vim");
527        });
528        cx.set_global::<CommandPaletteInterceptor>(CommandPaletteInterceptor(Box::new(
529            command::command_interceptor,
530        )));
531
532        if let Some(active_window) = cx
533            .active_window()
534            .and_then(|window| window.downcast::<Workspace>())
535        {
536            active_window
537                .update(cx, |workspace, cx| {
538                    let active_editor = workspace.active_item_as::<Editor>(cx);
539                    if let Some(active_editor) = active_editor {
540                        self.activate_editor(active_editor, cx);
541                        self.switch_mode(Mode::Normal, false, cx);
542                    }
543                })
544                .ok();
545        }
546    }
547
548    /// Returns the state of the active editor.
549    pub fn state(&self) -> &EditorState {
550        if let Some(active_editor) = self.active_editor.as_ref() {
551            if let Some(state) = self.editor_states.get(&active_editor.entity_id()) {
552                return state;
553            }
554        }
555
556        &self.default_state
557    }
558
559    /// Updates the state of the active editor.
560    pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
561        let mut state = self.state().clone();
562        let ret = func(&mut state);
563
564        if let Some(active_editor) = self.active_editor.as_ref() {
565            self.editor_states.insert(active_editor.entity_id(), state);
566        }
567
568        ret
569    }
570
571    fn sync_vim_settings(&mut self, cx: &mut WindowContext) {
572        self.update_active_editor(cx, |vim, editor, cx| {
573            let state = vim.state();
574            editor.set_cursor_shape(state.cursor_shape(), cx);
575            editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
576            editor.set_collapse_matches(true);
577            editor.set_input_enabled(!state.vim_controlled());
578            editor.set_autoindent(state.should_autoindent());
579            editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
580            if editor.is_focused(cx) {
581                editor.set_keymap_context_layer::<Self>(state.keymap_context_layer(), cx);
582            } else {
583                editor.remove_keymap_context_layer::<Self>(cx);
584            }
585        });
586    }
587
588    fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
589        if editor.mode() == EditorMode::Full {
590            editor.set_cursor_shape(CursorShape::Bar, cx);
591            editor.set_clip_at_line_ends(false, cx);
592            editor.set_collapse_matches(false);
593            editor.set_input_enabled(true);
594            editor.set_autoindent(true);
595            editor.selections.line_mode = false;
596        }
597        editor.remove_keymap_context_layer::<Self>(cx)
598    }
599}
600
601impl Settings for VimModeSetting {
602    const KEY: Option<&'static str> = Some("vim_mode");
603
604    type FileContent = Option<bool>;
605
606    fn load(
607        default_value: &Self::FileContent,
608        user_values: &[&Self::FileContent],
609        _: &mut AppContext,
610    ) -> Result<Self> {
611        Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
612            default_value.ok_or_else(Self::missing_default)?,
613        )))
614    }
615}
616
617/// Controls the soft-wrapping behavior in the editor.
618#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
619#[serde(rename_all = "snake_case")]
620pub enum UseSystemClipboard {
621    Never,
622    Always,
623    OnYank,
624}
625
626#[derive(Deserialize)]
627struct VimSettings {
628    // all vim uses vim clipboard
629    // vim always uses system cliupbaord
630    // some magic where yy is system and dd is not.
631    pub use_system_clipboard: UseSystemClipboard,
632}
633
634#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
635struct VimSettingsContent {
636    pub use_system_clipboard: Option<UseSystemClipboard>,
637}
638
639impl Settings for VimSettings {
640    const KEY: Option<&'static str> = Some("vim");
641
642    type FileContent = VimSettingsContent;
643
644    fn load(
645        default_value: &Self::FileContent,
646        user_values: &[&Self::FileContent],
647        _: &mut AppContext,
648    ) -> Result<Self> {
649        Self::load_via_json_merge(default_value, user_values)
650    }
651}
652
653fn local_selections_changed(
654    newest: Selection<usize>,
655    is_multicursor: bool,
656    cx: &mut WindowContext,
657) {
658    Vim::update(cx, |vim, cx| {
659        if vim.state().mode == Mode::Normal && !newest.is_empty() {
660            if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
661                vim.switch_mode(Mode::VisualBlock, false, cx);
662            } else {
663                vim.switch_mode(Mode::Visual, false, cx)
664            }
665        } else if newest.is_empty()
666            && !is_multicursor
667            && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&vim.state().mode)
668        {
669            vim.switch_mode(Mode::Normal, true, cx)
670        }
671    })
672}