vim lifecycle (#7647)

Release Notes:

- Fixed :0 and :% in vim mode
([#4303](https://github.com/zed-industries/zed/issues/4303)).
- Improved keymap loading to not load vim key bindings unless vim is
enabled

**or**

- N/A
This commit is contained in:
Conrad Irwin 2024-02-10 16:21:13 -07:00 committed by GitHub
parent a159183f52
commit bd882c66d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 103 additions and 118 deletions

View file

@ -498,7 +498,7 @@
} }
}, },
{ {
"context": "BufferSearchBar && !in_replace > VimEnabled", "context": "BufferSearchBar && !in_replace",
"bindings": { "bindings": {
"enter": "vim::SearchSubmit", "enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss" "escape": "buffer_search::Dismiss"

View file

@ -1,6 +1,7 @@
use crate::{insert::NormalBefore, Vim}; use crate::{insert::NormalBefore, Vim, VimModeSetting};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use gpui::{Action, AppContext, Entity, EntityId, View, ViewContext, WindowContext}; use gpui::{Action, AppContext, Entity, EntityId, View, ViewContext, WindowContext};
use settings::{Settings, SettingsStore};
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| { cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| {
@ -12,6 +13,17 @@ pub fn init(cx: &mut AppContext) {
}) })
.detach(); .detach();
let mut enabled = VimModeSetting::get_global(cx).0;
cx.observe_global::<SettingsStore>(move |editor, cx| {
if VimModeSetting::get_global(cx).0 != enabled {
enabled = VimModeSetting::get_global(cx).0;
if !enabled {
Vim::unhook_vim_settings(editor, cx);
}
}
})
.detach();
let id = cx.view().entity_id(); let id = cx.view().entity_id();
cx.on_release(move |_, _, cx| released(id, cx)).detach(); cx.on_release(move |_, _, cx| released(id, cx)).detach();
}) })
@ -19,34 +31,25 @@ pub fn init(cx: &mut AppContext) {
} }
fn focused(editor: View<Editor>, cx: &mut WindowContext) { fn focused(editor: View<Editor>, cx: &mut WindowContext) {
if Vim::read(cx).active_editor.clone().is_some() {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |previously_active_editor, cx| {
vim.unhook_vim_settings(previously_active_editor, cx)
});
});
}
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.set_active_editor(editor.clone(), cx); if !vim.enabled {
return;
}
vim.activate_editor(editor.clone(), cx);
}); });
} }
fn blurred(editor: View<Editor>, cx: &mut WindowContext) { fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.stop_recording_immediately(NormalBefore.boxed_clone());
if let Some(previous_editor) = vim.active_editor.clone() { if let Some(previous_editor) = vim.active_editor.clone() {
vim.stop_recording_immediately(NormalBefore.boxed_clone());
if previous_editor if previous_editor
.upgrade() .upgrade()
.is_some_and(|previous| previous == editor.clone()) .is_some_and(|previous| previous == editor.clone())
{ {
vim.clear_operator(cx); vim.clear_operator(cx);
vim.active_editor = None;
vim.editor_subscription = None;
} }
} }
editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx))
}); });
} }

View file

@ -1,5 +1,4 @@
use gpui::{div, Element, Render, Subscription, ViewContext}; use gpui::{div, Element, Render, Subscription, ViewContext};
use settings::SettingsStore;
use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
use crate::{state::Mode, Vim}; use crate::{state::Mode, Vim};
@ -7,20 +6,16 @@ use crate::{state::Mode, Vim};
/// The ModeIndicator displays the current mode in the status bar. /// The ModeIndicator displays the current mode in the status bar.
pub struct ModeIndicator { pub struct ModeIndicator {
pub(crate) mode: Option<Mode>, pub(crate) mode: Option<Mode>,
_subscriptions: Vec<Subscription>, _subscription: Subscription,
} }
impl ModeIndicator { impl ModeIndicator {
/// Construct a new mode indicator in this window. /// Construct a new mode indicator in this window.
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
let _subscriptions = vec![ let _subscription = cx.observe_global::<Vim>(|this, cx| this.update_mode(cx));
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
cx.observe_global::<SettingsStore>(|this, cx| this.update_mode(cx)),
];
let mut this = Self { let mut this = Self {
mode: None, mode: None,
_subscriptions, _subscription,
}; };
this.update_mode(cx); this.update_mode(cx);
this this

View file

@ -169,7 +169,6 @@ impl EditorState {
pub fn keymap_context_layer(&self) -> KeyContext { pub fn keymap_context_layer(&self) -> KeyContext {
let mut context = KeyContext::default(); let mut context = KeyContext::default();
context.add("VimEnabled");
context.set( context.set(
"vim_mode", "vim_mode",
match self.mode { match self.mode {

View file

@ -49,7 +49,9 @@ impl VimTestContext {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled)); store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
}); });
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); if enabled {
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
}
}); });
// Setup search toolbars and keypress hook // Setup search toolbars and keypress hook

View file

@ -20,8 +20,8 @@ use command_palette::CommandPaletteInterceptor;
use copilot::CommandPaletteFilter; use copilot::CommandPaletteFilter;
use editor::{movement, Editor, EditorEvent, EditorMode}; use editor::{movement, Editor, EditorEvent, EditorMode};
use gpui::{ use gpui::{
actions, impl_actions, Action, AppContext, EntityId, Global, KeyContext, Subscription, View, actions, impl_actions, Action, AppContext, EntityId, Global, Subscription, View, ViewContext,
ViewContext, WeakView, WindowContext, WeakView, WindowContext,
}; };
use language::{CursorShape, Point, Selection, SelectionGoal}; use language::{CursorShape, Point, Selection, SelectionGoal};
pub use mode_indicator::ModeIndicator; pub use mode_indicator::ModeIndicator;
@ -197,7 +197,11 @@ impl Vim {
cx.update_global(update) cx.update_global(update)
} }
fn set_active_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) { fn activate_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
if editor.read(cx).mode() != EditorMode::Full {
return;
}
self.active_editor = Some(editor.clone().downgrade()); self.active_editor = Some(editor.clone().downgrade());
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
EditorEvent::SelectionsChanged { local: true } => { EditorEvent::SelectionsChanged { local: true } => {
@ -219,19 +223,17 @@ impl Vim {
_ => {} _ => {}
})); }));
if self.enabled { let editor = editor.read(cx);
let editor = editor.read(cx); let editor_mode = editor.mode();
let editor_mode = editor.mode(); let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
if editor_mode == EditorMode::Full if editor_mode == EditorMode::Full
&& !newest_selection_empty && !newest_selection_empty
&& self.state().mode == Mode::Normal && self.state().mode == Mode::Normal
// When following someone, don't switch vim mode. // When following someone, don't switch vim mode.
&& editor.leader_peer_id().is_none() && editor.leader_peer_id().is_none()
{ {
self.switch_mode(Mode::Visual, true, cx); self.switch_mode(Mode::Visual, true, cx);
}
} }
self.sync_vim_settings(cx); self.sync_vim_settings(cx);
@ -504,43 +506,39 @@ impl Vim {
} }
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
if self.enabled != enabled { if self.enabled == enabled {
self.enabled = enabled; return;
}
if !enabled {
let _ = cx.remove_global::<CommandPaletteInterceptor>();
cx.update_global::<CommandPaletteFilter, _>(|filter, _| { cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
if self.enabled { filter.hidden_namespaces.insert("vim");
filter.hidden_namespaces.remove("vim");
} else {
filter.hidden_namespaces.insert("vim");
}
}); });
*self = Default::default();
return;
}
if self.enabled { self.enabled = true;
cx.set_global::<CommandPaletteInterceptor>(CommandPaletteInterceptor(Box::new( cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
command::command_interceptor, filter.hidden_namespaces.remove("vim");
))); });
} else if cx.has_global::<CommandPaletteInterceptor>() { cx.set_global::<CommandPaletteInterceptor>(CommandPaletteInterceptor(Box::new(
let _ = cx.remove_global::<CommandPaletteInterceptor>(); command::command_interceptor,
} )));
if let Some(active_window) = cx.active_window() { if let Some(active_window) = cx
active_window .active_window()
.update(cx, |root_view, cx| { .and_then(|window| window.downcast::<Workspace>())
if self.enabled { {
let active_editor = root_view active_window
.downcast::<Workspace>() .update(cx, |workspace, cx| {
.ok() let active_editor = workspace.active_item_as::<Editor>(cx);
.and_then(|workspace| workspace.read(cx).active_item(cx)) if let Some(active_editor) = active_editor {
.and_then(|item| item.downcast::<Editor>()); self.activate_editor(active_editor, cx);
if let Some(active_editor) = active_editor { self.switch_mode(Mode::Normal, false, cx);
self.set_active_editor(active_editor, cx); }
} })
self.switch_mode(Mode::Normal, false, cx); .ok();
}
self.sync_vim_settings(cx);
})
.ok();
}
} }
} }
@ -569,45 +567,29 @@ impl Vim {
fn sync_vim_settings(&self, cx: &mut WindowContext) { fn sync_vim_settings(&self, cx: &mut WindowContext) {
let state = self.state(); let state = self.state();
let cursor_shape = state.cursor_shape();
self.update_active_editor(cx, |editor, cx| { self.update_active_editor(cx, |editor, cx| {
if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(state.cursor_shape(), cx);
editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true);
editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled());
editor.set_input_enabled(!state.vim_controlled()); editor.set_autoindent(state.should_autoindent());
editor.set_autoindent(state.should_autoindent()); editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); let context_layer = state.keymap_context_layer();
let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::<Self>(context_layer, cx);
editor.set_keymap_context_layer::<Self>(context_layer, cx);
} else {
// Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
// but we need collapse_matches to persist when the search bar is focused.
editor.set_collapse_matches(false);
self.unhook_vim_settings(editor, cx);
}
}); });
} }
fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) { fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
editor.set_cursor_shape(CursorShape::Bar, cx); if editor.mode() == EditorMode::Full {
editor.set_clip_at_line_ends(false, cx); editor.set_cursor_shape(CursorShape::Bar, cx);
editor.set_input_enabled(true); editor.set_clip_at_line_ends(false, cx);
editor.set_autoindent(true); editor.set_collapse_matches(false);
editor.selections.line_mode = false; editor.set_input_enabled(true);
editor.set_autoindent(true);
// we set the VimEnabled context on all editors so that we editor.selections.line_mode = false;
// can distinguish between vim mode and non-vim mode in the BufferSearchBar.
// This is a bit of a hack, but currently the search crate does not depend on vim,
// and it seems nice to keep it that way.
if self.enabled {
let mut context = KeyContext::default();
context.add("VimEnabled");
editor.set_keymap_context_layer::<Self>(context, cx)
} else {
editor.remove_keymap_context_layer::<Self>(cx);
} }
editor.remove_keymap_context_layer::<Self>(cx)
} }
} }
@ -633,19 +615,17 @@ fn local_selections_changed(
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
if vim.enabled { if vim.state().mode == Mode::Normal && !newest.is_empty() {
if vim.state().mode == Mode::Normal && !newest.is_empty() { if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { vim.switch_mode(Mode::VisualBlock, false, cx);
vim.switch_mode(Mode::VisualBlock, false, cx); } else {
} else { vim.switch_mode(Mode::Visual, false, cx)
vim.switch_mode(Mode::Visual, false, cx)
}
} else if newest.is_empty()
&& !is_multicursor
&& [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&vim.state().mode)
{
vim.switch_mode(Mode::Normal, true, cx)
} }
} else if newest.is_empty()
&& !is_multicursor
&& [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&vim.state().mode)
{
vim.switch_mode(Mode::Normal, true, cx)
} }
}) })
} }

View file

@ -32,6 +32,7 @@ use util::{
ResultExt, ResultExt,
}; };
use uuid::Uuid; use uuid::Uuid;
use vim::VimModeSetting;
use welcome::BaseKeymap; use welcome::BaseKeymap;
use workspace::Pane; use workspace::Pane;
use workspace::{ use workspace::{
@ -495,13 +496,17 @@ pub fn handle_keymap_file_changes(
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
BaseKeymap::register(cx); BaseKeymap::register(cx);
VimModeSetting::register(cx);
let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded(); let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
let mut old_base_keymap = *BaseKeymap::get_global(cx); let mut old_base_keymap = *BaseKeymap::get_global(cx);
let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
cx.observe_global::<SettingsStore>(move |cx| { cx.observe_global::<SettingsStore>(move |cx| {
let new_base_keymap = *BaseKeymap::get_global(cx); let new_base_keymap = *BaseKeymap::get_global(cx);
if new_base_keymap != old_base_keymap { let new_vim_enabled = VimModeSetting::get_global(cx).0;
if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
old_base_keymap = new_base_keymap.clone(); old_base_keymap = new_base_keymap.clone();
old_vim_enabled = new_vim_enabled;
base_keymap_tx.unbounded_send(()).unwrap(); base_keymap_tx.unbounded_send(()).unwrap();
} }
}) })
@ -538,8 +543,9 @@ fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
} }
pub fn load_default_keymap(cx: &mut AppContext) { pub fn load_default_keymap(cx: &mut AppContext) {
for path in ["keymaps/default.json", "keymaps/vim.json"] { KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
KeymapFile::load_asset(path, cx).unwrap(); if VimModeSetting::get_global(cx).0 {
KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
} }
if let Some(asset_path) = BaseKeymap::get_global(cx).asset_path() { if let Some(asset_path) = BaseKeymap::get_global(cx).asset_path() {