1868 lines · 67.3 KB · Rust
RawDownload
//! Vim support for Zed. #[cfg(test)]mod test; mod change_list;mod command;mod digraph;mod helix;mod indent;mod insert;mod mode_indicator;mod motion;mod normal;mod object;mod replace;mod rewrap;mod state;mod surrounds;mod visual; use anyhow::Result;use collections::HashMap;use editor::{    Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects,    ToPoint,    movement::{self, FindRange},};use gpui::{    Action, App, AppContext, Axis, Context, Entity, EventEmitter, KeyContext, KeystrokeEvent,    Render, Subscription, Task, WeakEntity, Window, actions,};use insert::{NormalBefore, TemporaryNormal};use language::{CharKind, CursorShape, Point, Selection, SelectionGoal, TransactionId};pub use mode_indicator::ModeIndicator;use motion::Motion;use normal::search::SearchSubmit;use object::Object;use schemars::JsonSchema;use serde::Deserialize;use serde_derive::Serialize;use settings::{Settings, SettingsSources, SettingsStore, update_settings_file};use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};use std::{mem, ops::Range, sync::Arc};use surrounds::SurroundsType;use theme::ThemeSettings;use ui::{IntoElement, SharedString, px};use vim_mode_setting::HelixModeSetting;use vim_mode_setting::VimModeSetting;use workspace::{self, Pane, Workspace}; use crate::state::ReplayableAction; /// Number is used to manage vim's count. Pushing a digit/// multiplies the current value by 10 and adds the digit.#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]struct Number(usize); #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]struct SelectRegister(String); #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushObject {    around: bool,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushFindForward {    before: bool,    multiline: bool,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushFindBackward {    after: bool,    multiline: bool,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushSneak {    first_char: Option<char>,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushSneakBackward {    first_char: Option<char>,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushAddSurrounds; #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushChangeSurrounds {    target: Option<Object>,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushJump {    line: bool,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushDigraph {    first_char: Option<char>,} #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]#[action(namespace = vim)]#[serde(deny_unknown_fields)]struct PushLiteral {    prefix: Option<String>,} actions!(    vim,    [        /// Switches to normal mode.        SwitchToNormalMode,        /// Switches to insert mode.        SwitchToInsertMode,        /// Switches to replace mode.        SwitchToReplaceMode,        /// Switches to visual mode.        SwitchToVisualMode,        /// Switches to visual line mode.        SwitchToVisualLineMode,        /// Switches to visual block mode.        SwitchToVisualBlockMode,        /// Switches to Helix-style normal mode.        SwitchToHelixNormalMode,        /// Clears any pending operators.        ClearOperators,        /// Clears the exchange register.        ClearExchange,        /// Inserts a tab character.        Tab,        /// Inserts a newline.        Enter,        /// Selects inner text object.        InnerObject,        /// Maximizes the current pane.        MaximizePane,        /// Opens the default keymap file.        OpenDefaultKeymap,        /// Resets all pane sizes to default.        ResetPaneSizes,        /// Resizes the pane to the right.        ResizePaneRight,        /// Resizes the pane to the left.        ResizePaneLeft,        /// Resizes the pane upward.        ResizePaneUp,        /// Resizes the pane downward.        ResizePaneDown,        /// Starts a change operation.        PushChange,        /// Starts a delete operation.        PushDelete,        /// Exchanges text regions.        Exchange,        /// Starts a yank operation.        PushYank,        /// Starts a replace operation.        PushReplace,        /// Deletes surrounding characters.        PushDeleteSurrounds,        /// Sets a mark at the current position.        PushMark,        /// Toggles the marks view.        ToggleMarksView,        /// Starts a forced motion.        PushForcedMotion,        /// Starts an indent operation.        PushIndent,        /// Starts an outdent operation.        PushOutdent,        /// Starts an auto-indent operation.        PushAutoIndent,        /// Starts a rewrap operation.        PushRewrap,        /// Starts a shell command operation.        PushShellCommand,        /// Converts to lowercase.        PushLowercase,        /// Converts to uppercase.        PushUppercase,        /// Toggles case.        PushOppositeCase,        /// Applies ROT13 encoding.        PushRot13,        /// Applies ROT47 encoding.        PushRot47,        /// Toggles the registers view.        ToggleRegistersView,        /// Selects a register.        PushRegister,        /// Starts recording to a register.        PushRecordRegister,        /// Replays a register.        PushReplayRegister,        /// Replaces with register contents.        PushReplaceWithRegister,        /// Toggles comments.        PushToggleComments,    ]); // in the workspace namespace so it's not filtered out when vim is disabled.actions!(    workspace,    [        /// Toggles Vim mode on or off.        ToggleVimMode,    ]); /// Initializes the `vim` crate.pub fn init(cx: &mut App) {    vim_mode_setting::init(cx);    VimSettings::register(cx);    VimGlobals::register(cx);     cx.observe_new(Vim::register).detach();     cx.observe_new(|workspace: &mut Workspace, _, _| {        workspace.register_action(|workspace, _: &ToggleVimMode, _, cx| {            let fs = workspace.app_state().fs.clone();            let currently_enabled = Vim::enabled(cx);            update_settings_file::<VimModeSetting>(fs, cx, move |setting, _| {                *setting = Some(!currently_enabled)            })        });         workspace.register_action(|_, _: &OpenDefaultKeymap, _, cx| {            cx.emit(workspace::Event::OpenBundledFile {                text: settings::vim_keymap(),                title: "Default Vim Bindings",                language: "JSON",            });        });         workspace.register_action(|workspace, _: &ResetPaneSizes, _, cx| {            workspace.reset_pane_sizes(cx);        });         workspace.register_action(|workspace, _: &MaximizePane, window, cx| {            let pane = workspace.active_pane();            let Some(size) = workspace.bounding_box_for_pane(&pane) else {                return;            };             let theme = ThemeSettings::get_global(cx);            let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();             let desired_size = if let Some(count) = Vim::take_count(cx) {                height * count            } else {                px(10000.)            };            workspace.resize_pane(Axis::Vertical, desired_size - size.size.height, window, cx)        });         workspace.register_action(|workspace, _: &ResizePaneRight, window, cx| {            let count = Vim::take_count(cx).unwrap_or(1) as f32;            Vim::take_forced_motion(cx);            let theme = ThemeSettings::get_global(cx);            let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {                return;            };            let Ok(width) = window                .text_system()                .advance(font_id, theme.buffer_font_size(cx), 'm')            else {                return;            };            workspace.resize_pane(Axis::Horizontal, width.width * count, window, cx);        });         workspace.register_action(|workspace, _: &ResizePaneLeft, window, cx| {            let count = Vim::take_count(cx).unwrap_or(1) as f32;            Vim::take_forced_motion(cx);            let theme = ThemeSettings::get_global(cx);            let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {                return;            };            let Ok(width) = window                .text_system()                .advance(font_id, theme.buffer_font_size(cx), 'm')            else {                return;            };            workspace.resize_pane(Axis::Horizontal, -width.width * count, window, cx);        });         workspace.register_action(|workspace, _: &ResizePaneUp, window, cx| {            let count = Vim::take_count(cx).unwrap_or(1) as f32;            Vim::take_forced_motion(cx);            let theme = ThemeSettings::get_global(cx);            let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();            workspace.resize_pane(Axis::Vertical, height * count, window, cx);        });         workspace.register_action(|workspace, _: &ResizePaneDown, window, cx| {            let count = Vim::take_count(cx).unwrap_or(1) as f32;            Vim::take_forced_motion(cx);            let theme = ThemeSettings::get_global(cx);            let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();            workspace.resize_pane(Axis::Vertical, -height * count, window, cx);        });         workspace.register_action(|workspace, _: &SearchSubmit, window, cx| {            let vim = workspace                .focused_pane(window, cx)                .read(cx)                .active_item()                .and_then(|item| item.act_as::<Editor>(cx))                .and_then(|editor| editor.read(cx).addon::<VimAddon>().cloned());            let Some(vim) = vim else { return };            vim.entity.update(cx, |_, cx| {                cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx))            })        });    })    .detach();} #[derive(Clone)]pub(crate) struct VimAddon {    pub(crate) entity: Entity<Vim>,} impl editor::Addon for VimAddon {    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {        self.entity.read(cx).extend_key_context(key_context, cx)    }     fn to_any(&self) -> &dyn std::any::Any {        self    }} /// The state pertaining to Vim mode.pub(crate) struct Vim {    pub(crate) mode: Mode,    pub last_mode: Mode,    pub temp_mode: bool,    pub status_label: Option<SharedString>,    pub exit_temporary_mode: bool,     operator_stack: Vec<Operator>,    pub(crate) replacements: Vec<(Range<editor::Anchor>, String)>,     pub(crate) stored_visual_mode: Option<(Mode, Vec<bool>)>,     pub(crate) current_tx: Option<TransactionId>,    pub(crate) current_anchor: Option<Selection<Anchor>>,    pub(crate) undo_modes: HashMap<TransactionId, Mode>,    pub(crate) undo_last_line_tx: Option<TransactionId>,     selected_register: Option<char>,    pub search: SearchState,     editor: WeakEntity<Editor>,     last_command: Option<String>,    running_command: Option<Task<()>>,    _subscriptions: Vec<Subscription>,} // Hack: Vim intercepts events dispatched to a window and updates the view in response.// This means it needs a VisualContext. The easiest way to satisfy that constraint is// to make Vim a "View" that is just never actually rendered.impl Render for Vim {    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {        gpui::Empty    }} enum VimEvent {    Focused,}impl EventEmitter<VimEvent> for Vim {} impl Vim {    /// The namespace for Vim actions.    const NAMESPACE: &'static str = "vim";     pub fn new(window: &mut Window, cx: &mut Context<Editor>) -> Entity<Self> {        let editor = cx.entity().clone();         let mut initial_mode = VimSettings::get_global(cx).default_mode;        if initial_mode == Mode::Normal && HelixModeSetting::get_global(cx).0 {            initial_mode = Mode::HelixNormal;        }         cx.new(|cx| Vim {            mode: initial_mode,            last_mode: Mode::Normal,            temp_mode: false,            exit_temporary_mode: false,            operator_stack: Vec::new(),            replacements: Vec::new(),             stored_visual_mode: None,            current_tx: None,            undo_last_line_tx: None,            current_anchor: None,            undo_modes: HashMap::default(),             status_label: None,            selected_register: None,            search: SearchState::default(),             last_command: None,            running_command: None,             editor: editor.downgrade(),            _subscriptions: vec![                cx.observe_keystrokes(Self::observe_keystrokes),                cx.subscribe_in(&editor, window, |this, _, event, window, cx| {                    this.handle_editor_event(event, window, cx)                }),            ],        })    }     fn register(editor: &mut Editor, window: Option<&mut Window>, cx: &mut Context<Editor>) {        let Some(window) = window else {            return;        };         if !editor.use_modal_editing() {            return;        }         let mut was_enabled = Vim::enabled(cx);        let mut was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;        cx.observe_global_in::<SettingsStore>(window, move |editor, window, cx| {            let enabled = Vim::enabled(cx);            let toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;            if enabled && was_enabled && (toggle != was_toggle) {                if toggle {                    let is_relative = editor                        .addon::<VimAddon>()                        .map(|vim| vim.entity.read(cx).mode != Mode::Insert);                    editor.set_relative_line_number(is_relative, cx)                } else {                    editor.set_relative_line_number(None, cx)                }            }            was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;            if was_enabled == enabled {                return;            }            was_enabled = enabled;            if enabled {                Self::activate(editor, window, cx)            } else {                Self::deactivate(editor, cx)            }        })        .detach();        if was_enabled {            Self::activate(editor, window, cx)        }    }     fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>) {        let vim = Vim::new(window, cx);         if !editor.mode().is_full() {            vim.update(cx, |vim, _| {                vim.mode = Mode::Insert;            });        }         editor.register_addon(VimAddon {            entity: vim.clone(),        }); 

Lines 1–500 of 1868.