vim: Improve lifecycle (#16477)

Closes #13579

A major painpoint in the Vim crate has been life-cycle management. We
used to have one global Vim instance that tried to track per-editor
state; this led to a number of subtle issues (e.g. #13579, the mode
indicator being global, and quick toggling between windows letting vim
mode's notion of the active editor get out of sync).

This PR changes the internal structure of the code so that there is now
one `Vim` instance per `Editor` (stored as an `Addon`); and the global
stuff is separated out. This fixes the above problems, and tidies up a
bunch of the mess in the codebase.

Release Notes:

* vim: Fixed accidental visual mode in project search and go to
references
([#13579](https://github.com/zed-industries/zed/issues/13579)).
This commit is contained in:
Conrad Irwin 2024-08-20 20:48:50 -06:00 committed by GitHub
parent c4c07583c3
commit 36d51fe4a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 3362 additions and 3585 deletions

View File

@ -462,6 +462,14 @@ struct ResolvedTasks {
struct MultiBufferOffset(usize);
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
struct BufferOffset(usize);
// Addons allow storing per-editor state in other crates (e.g. Vim)
pub trait Addon: 'static {
fn extend_key_context(&self, _: &mut KeyContext, _: &AppContext) {}
fn to_any(&self) -> &dyn std::any::Any;
}
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@ -533,7 +541,6 @@ pub struct Editor {
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
use_modal_editing: bool,
read_only: bool,
@ -551,7 +558,6 @@ pub struct Editor {
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
gutter_dimensions: GutterDimensions,
pub vim_replace_map: HashMap<Range<usize>, String>,
style: Option<EditorStyle>,
next_editor_action_id: EditorActionId,
editor_actions: Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut ViewContext<Self>)>>>>,
@ -581,6 +587,7 @@ pub struct Editor {
breadcrumb_header: Option<String>,
focused_block: Option<FocusedBlock>,
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@ -1875,7 +1882,6 @@ impl Editor {
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
keymap_context_layers: Default::default(),
input_enabled: true,
use_modal_editing: mode == EditorMode::Full,
read_only: false,
@ -1900,7 +1906,6 @@ impl Editor {
hovered_cursors: Default::default(),
next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
show_git_blame_gutter: false,
@ -1939,6 +1944,7 @@ impl Editor {
breadcrumb_header: None,
focused_block: None,
next_scroll_position: NextScrollCursorCenterTopBottom::default(),
addons: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@ -1961,13 +1967,13 @@ impl Editor {
this
}
pub fn mouse_menu_is_focused(&self, cx: &mut WindowContext) -> bool {
pub fn mouse_menu_is_focused(&self, cx: &WindowContext) -> bool {
self.mouse_context_menu
.as_ref()
.is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(cx))
}
fn key_context(&self, cx: &AppContext) -> KeyContext {
fn key_context(&self, cx: &ViewContext<Self>) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("Editor");
let mode = match self.mode {
@ -1998,8 +2004,13 @@ impl Editor {
}
}
for layer in self.keymap_context_layers.values() {
key_context.extend(layer);
// Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused.
if !self.focus_handle(cx).contains_focused(cx)
|| (self.is_focused(cx) || self.mouse_menu_is_focused(cx))
{
for addon in self.addons.values() {
addon.extend_key_context(&mut key_context, cx)
}
}
if let Some(extension) = self
@ -2241,21 +2252,6 @@ impl Editor {
}
}
pub fn set_keymap_context_layer<Tag: 'static>(
&mut self,
context: KeyContext,
cx: &mut ViewContext<Self>,
) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
cx.notify();
}
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
cx.notify();
}
pub fn set_input_enabled(&mut self, input_enabled: bool) {
self.input_enabled = input_enabled;
}
@ -11864,7 +11860,6 @@ impl Editor {
self.editor_actions.borrow_mut().insert(
id,
Box::new(move |cx| {
let _view = cx.view().clone();
let cx = cx.window_context();
let listener = listener.clone();
cx.on_action(TypeId::of::<A>(), move |action, phase, cx| {
@ -11950,6 +11945,22 @@ impl Editor {
menu.visible() && matches!(menu, ContextMenu::Completions(_))
})
}
pub fn register_addon<T: Addon>(&mut self, instance: T) {
self.addons
.insert(std::any::TypeId::of::<T>(), Box::new(instance));
}
pub fn unregister_addon<T: Addon>(&mut self) {
self.addons.remove(&std::any::TypeId::of::<T>());
}
pub fn addon<T: Addon>(&self) -> Option<&T> {
let type_id = std::any::TypeId::of::<T>();
self.addons
.get(&type_id)
.and_then(|item| item.to_any().downcast_ref::<T>())
}
}
fn hunks_for_selections(

View File

@ -5613,7 +5613,7 @@ impl Element for EditorElement {
cx: &mut WindowContext,
) {
let focus_handle = self.editor.focus_handle(cx);
let key_context = self.editor.read(cx).key_context(cx);
let key_context = self.editor.update(cx, |editor, cx| editor.key_context(cx));
cx.set_key_context(key_context);
cx.handle_input(
&focus_handle,

View File

@ -1,48 +1,37 @@
use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor};
use gpui::{actions, View};
use ui::{ViewContext, WindowContext};
use workspace::Workspace;
use gpui::{actions, ViewContext};
use crate::{state::Mode, Vim};
actions!(vim, [ChangeListOlder, ChangeListNewer]);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_, _: &ChangeListOlder, cx| {
Vim::update(cx, |vim, cx| {
move_to_change(vim, Direction::Prev, cx);
})
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &ChangeListOlder, cx| {
vim.move_to_change(Direction::Prev, cx);
});
workspace.register_action(|_, _: &ChangeListNewer, cx| {
Vim::update(cx, |vim, cx| {
move_to_change(vim, Direction::Next, cx);
})
Vim::action(editor, cx, |vim, _: &ChangeListNewer, cx| {
vim.move_to_change(Direction::Next, cx);
});
}
fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) {
let count = vim.take_count(cx).unwrap_or(1);
let selections = vim.update_state(|state| {
if state.change_list.is_empty() {
return None;
impl Vim {
fn move_to_change(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx).unwrap_or(1);
if self.change_list.is_empty() {
return;
}
let prev = state
.change_list_position
.unwrap_or(state.change_list.len());
let prev = self.change_list_position.unwrap_or(self.change_list.len());
let next = if direction == Direction::Prev {
prev.saturating_sub(count)
} else {
(prev + count).min(state.change_list.len() - 1)
(prev + count).min(self.change_list.len() - 1)
};
state.change_list_position = Some(next);
state.change_list.get(next).cloned()
});
let Some(selections) = selections else {
self.change_list_position = Some(next);
let Some(selections) = self.change_list.get(next).cloned() else {
return;
};
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.into_iter().map(|a| {
@ -53,12 +42,14 @@ fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) {
});
}
pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut WindowContext) {
let (map, selections) =
editor.update(cx, |editor, cx| editor.selections.all_adjusted_display(cx));
pub(crate) fn push_to_change_list(&mut self, cx: &mut ViewContext<Self>) {
let Some((map, selections)) = self.update_editor(cx, |_, editor, cx| {
editor.selections.all_adjusted_display(cx)
}) else {
return;
};
let pop_state =
vim.state()
let pop_state = self
.change_list
.last()
.map(|previous| {
@ -72,7 +63,7 @@ pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut
let new_positions = selections
.into_iter()
.map(|s| {
let point = if vim.state().mode == Mode::Insert {
let point = if self.mode == Mode::Insert {
movement::saturating_left(&map, s.head())
} else {
s.head()
@ -81,13 +72,12 @@ pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut
})
.collect();
vim.update_state(|state| {
state.change_list_position.take();
self.change_list_position.take();
if pop_state {
state.change_list.pop();
self.change_list.pop();
}
self.change_list.push(new_positions);
}
state.change_list.push(new_positions);
})
}
#[cfg(test)]

View File

@ -12,12 +12,11 @@ use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use ui::WindowContext;
use util::ResultExt;
use workspace::{notifications::NotifyResultExt, SaveIntent, Workspace};
use workspace::{notifications::NotifyResultExt, SaveIntent};
use crate::{
motion::{EndOfDocument, Motion, StartOfDocument},
normal::{
move_cursor,
search::{FindCommand, ReplaceCommand, Replacement},
JoinLines,
},
@ -66,77 +65,89 @@ impl Clone for WithRange {
}
}
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &VisualCommand, cx| {
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &VisualCommand, cx| {
let Some(workspace) = vim.workspace(cx) else {
return;
};
workspace.update(cx, |workspace, cx| {
command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
})
});
workspace.register_action(|workspace, _: &CountCommand, cx| {
let count = Vim::update(cx, |vim, cx| vim.take_count(cx)).unwrap_or(1);
Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
let Some(workspace) = vim.workspace(cx) else {
return;
};
let count = vim.take_count(cx).unwrap_or(1);
workspace.update(cx, |workspace, cx| {
command_palette::CommandPalette::toggle(
workspace,
&format!(".,.+{}", count.saturating_sub(1)),
cx,
);
})
});
workspace.register_action(|workspace: &mut Workspace, action: &GoToLine, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, action: &GoToLine, cx| {
vim.switch_mode(Mode::Normal, false, cx);
let result = vim.update_active_editor(cx, |vim, editor, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
action.range.head().buffer_row(vim, editor, cx)
});
let Some(buffer_row) = result else {
return anyhow::Ok(());
let buffer_row = match result {
None => return,
Some(e @ Err(_)) => {
let Some(workspace) = vim.workspace(cx) else {
return;
};
move_cursor(
vim,
Motion::StartOfDocument,
Some(buffer_row?.0 as usize + 1),
cx,
);
Ok(())
})
.notify_err(workspace, cx);
workspace.update(cx, |workspace, cx| {
e.notify_err(workspace, cx);
});
return;
}
Some(Ok(result)) => result,
};
vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx);
});
workspace.register_action(|workspace: &mut Workspace, action: &WithRange, cx| {
Vim::action(editor, cx, |vim, action: &WithRange, cx| {
if action.is_count {
for _ in 0..action.range.as_count() {
cx.dispatch_action(action.action.boxed_clone())
}
} else {
Vim::update(cx, |vim, cx| {
let result = vim.update_active_editor(cx, |vim, editor, cx| {
return;
}
let result = vim.update_editor(cx, |vim, editor, cx| {
action.range.buffer_range(vim, editor, cx)
});
let Some(range) = result else {
return anyhow::Ok(());
let range = match result {
None => return,
Some(e @ Err(_)) => {
let Some(workspace) = vim.workspace(cx) else {
return;
};
let range = range?;
vim.update_active_editor(cx, |_, editor, cx| {
workspace.update(cx, |workspace, cx| {
e.notify_err(workspace, cx);
});
return;
}
Some(Ok(result)) => result,
};
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(None, cx, |s| {
let end = Point::new(range.end.0, s.buffer().line_len(range.end));
s.select_ranges([end..Point::new(range.start.0, 0)]);
})
});
cx.dispatch_action(action.action.boxed_clone());
cx.defer(move |cx| {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
cx.defer(move |vim, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([
Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
]);
s.select_ranges([Point::new(range.start.0, 0)..Point::new(range.start.0, 0)]);
})
});
})
});
Ok(())
})
.notify_err(workspace, cx);
}
});
}
@ -343,12 +354,7 @@ impl Position {
let target = match self {
Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
Position::Mark { name, offset } => {
let Some(mark) = vim
.state()
.marks
.get(&name.to_string())
.and_then(|vec| vec.last())
else {
let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
return Err(anyhow!("mark {} not set", name));
};
mark.to_point(&snapshot.buffer_snapshot)

View File

@ -4,7 +4,7 @@ use collections::HashMap;
use gpui::AppContext;
use settings::Settings;
use std::sync::LazyLock;
use ui::WindowContext;
use ui::ViewContext;
use crate::{Vim, VimSettings};
@ -34,16 +34,21 @@ fn lookup_digraph(a: char, b: char, cx: &AppContext) -> Arc<str> {
.unwrap_or_else(|| b.to_string().into())
}
pub fn insert_digraph(first_char: char, second_char: char, cx: &mut WindowContext) {
impl Vim {
pub fn insert_digraph(
&mut self,
first_char: char,
second_char: char,
cx: &mut ViewContext<Self>,
) {
let text = lookup_digraph(first_char, second_char, &cx);
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
if Vim::read(cx).state().editor_input_enabled() {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| editor.insert(&text, cx));
});
self.pop_operator(cx);
if self.editor_input_enabled() {
self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx));
} else {
Vim::active_editor_input_ignored(text, cx);
self.input_ignored(text, cx);
}
}
}

View File

@ -1,153 +0,0 @@
use crate::{insert::NormalBefore, Vim, VimModeSetting};
use editor::{Editor, EditorEvent};
use gpui::{Action, AppContext, Entity, EntityId, UpdateGlobal, View, ViewContext, WindowContext};
use settings::{Settings, SettingsStore};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| {
let editor = cx.view().clone();
cx.subscribe(&editor, |_, editor, event: &EditorEvent, cx| match event {
EditorEvent::Focused => cx.window_context().defer(|cx| focused(editor, cx)),
EditorEvent::Blurred => cx.window_context().defer(|cx| blurred(editor, cx)),
_ => {}
})
.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();
cx.on_release(move |_, _, cx| released(id, cx)).detach();
})
.detach();
}
fn focused(editor: View<Editor>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if !vim.enabled {
return;
}
vim.activate_editor(editor.clone(), cx);
});
}
fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if !vim.enabled {
return;
}
if let Some(previous_editor) = vim.active_editor.clone() {
vim.stop_recording_immediately(NormalBefore.boxed_clone());
if previous_editor
.upgrade()
.is_some_and(|previous| previous == editor.clone())
{
vim.store_visual_marks(cx);
vim.clear_operator(cx);
}
}
editor.update(cx, |editor, cx| {
if editor.use_modal_editing() {
editor.set_cursor_shape(language::CursorShape::Hollow, cx);
}
});
});
}
fn released(entity_id: EntityId, cx: &mut AppContext) {
Vim::update_global(cx, |vim, _cx| {
if vim
.active_editor
.as_ref()
.is_some_and(|previous| previous.entity_id() == entity_id)
{
vim.active_editor = None;
vim.editor_subscription = None;
}
vim.editor_states.remove(&entity_id)
});
}
#[cfg(test)]
mod test {
use crate::{test::VimTestContext, Vim};
use editor::Editor;
use gpui::{Context, Entity, VisualTestContext};
use language::Buffer;
// regression test for blur called with a different active editor
#[gpui::test]
async fn test_blur_focus(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let buffer = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx));
let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx));
let editor2 = cx
.update(|cx| {
window2.update(cx, |_, cx| {
cx.activate_window();
cx.focus_self();
cx.view().clone()
})
})
.unwrap();
cx.run_until_parked();
cx.update(|cx| {
let vim = Vim::read(cx);
assert_eq!(
vim.active_editor.as_ref().unwrap().entity_id(),
editor2.entity_id(),
)
});
// no panic when blurring an editor in a different window.
cx.update_editor(|editor1, cx| {
editor1.handle_blur(cx);
});
}
// regression test for focus_in/focus_out being called on window activation
#[gpui::test]
async fn test_focus_across_windows(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let mut cx1 = VisualTestContext::from_window(cx.window, &cx);
let editor1 = cx.editor.clone();
let buffer = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx));
let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx));
editor2.update(cx2, |_, cx| {
cx.focus_self();
cx.activate_window();
});
cx.run_until_parked();
cx1.update(|cx| {
assert_eq!(
Vim::read(cx).active_editor.as_ref().unwrap().entity_id(),
editor2.entity_id(),
)
});
cx1.update(|cx| {
cx.activate_window();
});
cx.run_until_parked();
cx.update(|cx| {
assert_eq!(
Vim::read(cx).active_editor.as_ref().unwrap().entity_id(),
editor1.entity_id(),
)
});
}
}

View File

@ -1,31 +1,26 @@
use crate::{
normal::{mark::create_mark, repeat},
state::Mode,
Vim,
};
use editor::{scroll::Autoscroll, Bias};
use crate::{state::Mode, Vim};
use editor::{scroll::Autoscroll, Bias, Editor};
use gpui::{actions, Action, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
actions!(vim, [NormalBefore]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(normal_before);
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, Vim::normal_before);
}
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
let should_repeat = Vim::update(cx, |vim, cx| {
if vim.state().active_operator().is_some() {
vim.update_state(|state| state.operator_stack.clear());
vim.sync_vim_settings(cx);
return false;
impl Vim {
fn normal_before(&mut self, action: &NormalBefore, cx: &mut ViewContext<Self>) {
if self.active_operator().is_some() {
self.operator_stack.clear();
self.sync_vim_settings(cx);
return;
}
let count = vim.take_count(cx).unwrap_or(1);
vim.stop_recording_immediately(action.boxed_clone());
if count <= 1 || vim.workspace_state.dot_replaying {
create_mark(vim, "^".into(), false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
let count = self.take_count(cx).unwrap_or(1);
self.stop_recording_immediately(action.boxed_clone(), cx);
if count <= 1 || Vim::globals(cx).dot_replaying {
self.create_mark("^".into(), false, cx);
self.update_editor(cx, |_, editor, cx| {
editor.dismiss_menus_and_popups(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
@ -34,15 +29,11 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
});
});
});
vim.switch_mode(Mode::Normal, false, cx);
false
} else {
true
self.switch_mode(Mode::Normal, false, cx);
return;
}
});
if should_repeat {
repeat::repeat(cx, true)
self.repeat(true, cx)
}
}

View File

@ -1,93 +1,95 @@
use gpui::{div, Element, Render, Subscription, ViewContext};
use gpui::{div, Element, Render, Subscription, View, ViewContext, WeakView};
use itertools::Itertools;
use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
use crate::{state::Mode, Vim};
use crate::{Vim, VimEvent};
/// The ModeIndicator displays the current mode in the status bar.
pub struct ModeIndicator {
pub(crate) mode: Option<Mode>,
pub(crate) operators: String,
vim: Option<WeakView<Vim>>,
pending_keys: Option<String>,
_subscriptions: Vec<Subscription>,
vim_subscription: Option<Subscription>,
}
impl ModeIndicator {
/// Construct a new mode indicator in this window.
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let _subscriptions = vec![
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
cx.observe_pending_input(|this, cx| {
this.update_pending_keys(cx);
cx.notify();
}),
];
})
.detach();
let mut this = Self {
mode: None,
operators: "".to_string(),
pending_keys: None,
_subscriptions,
};
this.update_mode(cx);
this
let handle = cx.view().clone();
let window = cx.window_handle();
cx.observe_new_views::<Vim>(move |_, cx| {
if cx.window_handle() != window {
return;
}
let vim = cx.view().clone();
handle.update(cx, |_, cx| {
cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event {
VimEvent::Focused => {
mode_indicator.vim_subscription =
Some(cx.observe(&vim, |_, _, cx| cx.notify()));
mode_indicator.vim = Some(vim.downgrade());
}
})
.detach()
})
})
.detach();
fn update_mode(&mut self, cx: &mut ViewContext<Self>) {
if let Some(vim) = self.vim(cx) {
self.mode = Some(vim.state().mode);
self.operators = self.current_operators_description(&vim);
} else {
self.mode = None;
Self {
vim: None,
pending_keys: None,
vim_subscription: None,
}
}
fn update_pending_keys(&mut self, cx: &mut ViewContext<Self>) {
if self.vim(cx).is_some() {
self.pending_keys = cx.pending_input_keystrokes().map(|keystrokes| {
keystrokes
.iter()
.map(|keystroke| format!("{}", keystroke))
.join(" ")
});
} else {
self.pending_keys = None;
}
}
fn vim<'a>(&self, cx: &'a mut ViewContext<Self>) -> Option<&'a Vim> {
// In some tests Vim isn't enabled, so we use try_global.
cx.try_global::<Vim>().filter(|vim| vim.enabled)
fn vim(&self) -> Option<View<Vim>> {
self.vim.as_ref().and_then(|vim| vim.upgrade())
}
fn current_operators_description(&self, vim: &Vim) -> String {
vim.workspace_state
fn current_operators_description(&self, vim: View<Vim>, cx: &mut ViewContext<Self>) -> String {
let recording = Vim::globals(cx)
.recording_register
.map(|reg| format!("recording @{reg} "))
.into_iter()
.chain(vim.state().pre_count.map(|count| format!("{}", count)))
.chain(vim.state().selected_register.map(|reg| format!("\"{reg}")))
.chain(
vim.state()
.operator_stack
.iter()
.map(|item| item.id().to_string()),
)
.chain(vim.state().post_count.map(|count| format!("{}", count)))
.into_iter();
let vim = vim.read(cx);
recording
.chain(vim.pre_count.map(|count| format!("{}", count)))
.chain(vim.selected_register.map(|reg| format!("\"{reg}")))
.chain(vim.operator_stack.iter().map(|item| item.id().to_string()))
.chain(vim.post_count.map(|count| format!("{}", count)))
.collect::<Vec<_>>()
.join("")
}
}
impl Render for ModeIndicator {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
let Some(mode) = self.mode.as_ref() else {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let vim = self.vim();
let Some(vim) = vim else {
return div().into_any();
};
let pending = self.pending_keys.as_ref().unwrap_or(&self.operators);
Label::new(format!("{} -- {} --", pending, mode))
let current_operators_description = self.current_operators_description(vim.clone(), cx);
let pending = self
.pending_keys
.as_ref()
.unwrap_or(&current_operators_description);
Label::new(format!("{} -- {} --", pending, vim.read(cx).mode))
.size(LabelSize::Small)
.line_height_style(LineHeightStyle::UiLabel)
.into_any_element()
@ -100,6 +102,5 @@ impl StatusItemView for ModeIndicator {
_active_pane_item: Option<&dyn ItemHandle>,
_cx: &mut ViewContext<Self>,
) {
// nothing to do.
}
}

View File

@ -4,20 +4,18 @@ use editor::{
self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
},
scroll::Autoscroll,
Anchor, Bias, DisplayPoint, RowExt, ToOffset,
Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
};
use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
use gpui::{actions, impl_actions, px, ViewContext};
use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use std::ops::Range;
use workspace::Workspace;
use crate::{
normal::{mark, normal_motion},
normal::mark,
state::{Mode, Operator},
surrounds::SurroundsType,
visual::visual_motion,
Vim,
};
@ -248,214 +246,227 @@ actions!(
]
);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
workspace
.register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
motion(
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx));
Vim::action(editor, cx, |vim, _: &Backspace, cx| {
vim.motion(Motion::Backspace, cx)
});
Vim::action(editor, cx, |vim, action: &Down, cx| {
vim.motion(
Motion::Down {
display_lines: action.display_lines,
},
cx,
)
});
workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
motion(
Vim::action(editor, cx, |vim, action: &Up, cx| {
vim.motion(
Motion::Up {
display_lines: action.display_lines,
},
cx,
)
});
workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
motion(
Vim::action(editor, cx, |vim, _: &Right, cx| {
vim.motion(Motion::Right, cx)
});
Vim::action(editor, cx, |vim, _: &Space, cx| {
vim.motion(Motion::Space, cx)
});
Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| {
vim.motion(
Motion::FirstNonWhitespace {
display_lines: action.display_lines,
},
cx,
)
});
workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
motion(
Vim::action(editor, cx, |vim, action: &StartOfLine, cx| {
vim.motion(
Motion::StartOfLine {
display_lines: action.display_lines,
},
cx,
)
});
workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
motion(
Vim::action(editor, cx, |vim, action: &EndOfLine, cx| {
vim.motion(
Motion::EndOfLine {
display_lines: action.display_lines,
},
cx,
)
});
workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
motion(Motion::CurrentLine, cx)
Vim::action(editor, cx, |vim, _: &CurrentLine, cx| {
vim.motion(Motion::CurrentLine, cx)
});
workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
motion(Motion::StartOfParagraph, cx)
Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| {
vim.motion(Motion::StartOfParagraph, cx)
});
workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
motion(Motion::EndOfParagraph, cx)
Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
vim.motion(Motion::EndOfParagraph, cx)
});
workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
motion(Motion::StartOfDocument, cx)
Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
vim.motion(Motion::StartOfDocument, cx)
});
workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
motion(Motion::EndOfDocument, cx)
Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| {
vim.motion(Motion::EndOfDocument, cx)
});
Vim::action(editor, cx, |vim, _: &Matching, cx| {
vim.motion(Motion::Matching, cx)
});
workspace
.register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
workspace.register_action(
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
motion(Motion::NextWordStart { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| {
vim.motion(Motion::NextWordStart { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
motion(Motion::NextWordEnd { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| {
vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace,
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
workspace.register_action(
|_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| {
vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &PreviousWordEnd { ignore_punctuation }, cx| {
vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| {
vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace,
&PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
);
workspace.register_action(
|_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
Vim::action(
editor,
cx,
|vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| {
vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
motion(Motion::NextLineStart, cx)
Vim::action(
editor,
cx,
|vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| {
vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx)
},
);
Vim::action(
editor,
cx,
|vim, &PreviousSubwordEnd { ignore_punctuation }, cx| {
vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
},
);
Vim::action(editor, cx, |vim, &NextLineStart, cx| {
vim.motion(Motion::NextLineStart, cx)
});
workspace.register_action(|_: &mut Workspace, &PreviousLineStart, cx: _| {
motion(Motion::PreviousLineStart, cx)
Vim::action(editor, cx, |vim, &PreviousLineStart, cx| {
vim.motion(Motion::PreviousLineStart, cx)
});
workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
motion(Motion::StartOfLineDownward, cx)
Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| {
vim.motion(Motion::StartOfLineDownward, cx)
});
workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
motion(Motion::EndOfLineDownward, cx)
Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| {
vim.motion(Motion::EndOfLineDownward, cx)
});
Vim::action(editor, cx, |vim, &GoToColumn, cx| {
vim.motion(Motion::GoToColumn, cx)
});
workspace
.register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
if let Some(last_find) = Vim::read(cx)
.workspace_state
.last_find
.clone()
.map(Box::new)
{
motion(Motion::RepeatFind { last_find }, cx);
Vim::action(editor, cx, |vim, _: &RepeatFind, cx| {
if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
vim.motion(Motion::RepeatFind { last_find }, cx);
}
});
workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
if let Some(last_find) = Vim::read(cx)
.workspace_state
.last_find
.clone()
.map(Box::new)
{
motion(Motion::RepeatFindReversed { last_find }, cx);
Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| {
if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
vim.motion(Motion::RepeatFindReversed { last_find }, cx);
}
});
workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
motion(Motion::WindowMiddle, cx)
Vim::action(editor, cx, |vim, &WindowTop, cx| {
vim.motion(Motion::WindowTop, cx)
});
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
motion(Motion::WindowBottom, cx)
Vim::action(editor, cx, |vim, &WindowMiddle, cx| {
vim.motion(Motion::WindowMiddle, cx)
});
Vim::action(editor, cx, |vim, &WindowBottom, cx| {
vim.motion(Motion::WindowBottom, cx)
});
}
pub(crate) fn search_motion(m: Motion, cx: &mut WindowContext) {
impl Vim {
pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext<Self>) {
if let Motion::ZedSearchResult {
prior_selections, ..
} = &m
{
match Vim::read(cx).state().mode {
match self.mode {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
if !prior_selections.is_empty() {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(prior_selections.iter().cloned())
})
});
});
}
}
Mode::Normal | Mode::Replace | Mode::Insert => {
if Vim::read(cx).active_operator().is_none() {
if self.active_operator().is_none() {
return;
}
}
}
}
motion(m, cx)
self.motion(m, cx)
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
Vim::read(cx).active_operator()
self.active_operator()
{
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
self.pop_operator(cx);
}
let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
let active_operator = Vim::read(cx).active_operator();
let count = self.take_count(cx);
let active_operator = self.active_operator();
let mut waiting_operator: Option<Operator> = None;
match Vim::read(cx).state().mode {
match self.mode {
Mode::Normal | Mode::Replace | Mode::Insert => {
if active_operator == Some(Operator::AddSurrounds { target: None }) {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Motion(motion)),
});
} else {
normal_motion(motion.clone(), active_operator.clone(), count, cx)
self.normal_motion(motion.clone(), active_operator.clone(), count, cx)
}
}
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
visual_motion(motion.clone(), count, cx)
self.visual_motion(motion.clone(), count, cx)
}
}
Vim::update(cx, |vim, cx| {
vim.clear_operator(cx);
self.clear_operator(cx);
if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
vim.update_state(|state| state.pre_count = count)
self.push_operator(operator, cx);
self.pre_count = count
}
}
});
}
// Motion handling is specified here:

View File

@ -19,30 +19,22 @@ use crate::{
motion::{self, first_non_whitespace, next_line_end, right, Motion},
object::Object,
state::{Mode, Operator},
surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType},
surrounds::SurroundsType,
Vim,
};
use case::{change_case_motion, change_case_object, CaseTarget};
use case::CaseTarget;
use collections::BTreeSet;
use editor::scroll::Autoscroll;
use editor::Anchor;
use editor::Bias;
use editor::Editor;
use editor::{display_map::ToDisplayPoint, movement};
use gpui::{actions, ViewContext, WindowContext};
use gpui::{actions, ViewContext};
use language::{Point, SelectionGoal};
use log::error;
use multi_buffer::MultiBufferRow;
use workspace::Workspace;
use self::{
case::{change_case, convert_to_lower_case, convert_to_upper_case},
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
indent::{indent_motion, indent_object, IndentDirection},
toggle_comments::{toggle_comments_motion, toggle_comments_object},
yank::{yank_motion, yank_object},
};
use self::indent::IndentDirection;
actions!(
vim,
@ -73,92 +65,80 @@ actions!(
]
);
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace.register_action(insert_after);
workspace.register_action(insert_before);
workspace.register_action(insert_first_non_whitespace);
workspace.register_action(insert_end_of_line);
workspace.register_action(insert_line_above);
workspace.register_action(insert_line_below);
workspace.register_action(insert_at_previous);
workspace.register_action(change_case);
workspace.register_action(convert_to_upper_case);
workspace.register_action(convert_to_lower_case);
workspace.register_action(yank_line);
workspace.register_action(yank_to_end_of_line);
workspace.register_action(toggle_comments);
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, Vim::insert_after);
Vim::action(editor, cx, Vim::insert_before);
Vim::action(editor, cx, Vim::insert_first_non_whitespace);
Vim::action(editor, cx, Vim::insert_end_of_line);
Vim::action(editor, cx, Vim::insert_line_above);
Vim::action(editor, cx, Vim::insert_line_below);
Vim::action(editor, cx, Vim::insert_at_previous);
Vim::action(editor, cx, Vim::change_case);
Vim::action(editor, cx, Vim::convert_to_upper_case);
Vim::action(editor, cx, Vim::convert_to_lower_case);
Vim::action(editor, cx, Vim::yank_line);
Vim::action(editor, cx, Vim::yank_to_end_of_line);
Vim::action(editor, cx, Vim::toggle_comments);
Vim::action(editor, cx, Vim::paste);
workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(vim, Motion::Left, times, cx);
})
vim.delete_motion(Motion::Left, times, cx);
});
workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &DeleteRight, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(vim, Motion::Right, times, cx);
})
vim.delete_motion(Motion::Right, times, cx);
});
workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| {
vim.start_recording(cx);
let times = vim.take_count(cx);
change_motion(
vim,
vim.change_motion(
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(
vim,
vim.delete_motion(
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
vim.record_current_action(cx);
let mut times = vim.take_count(cx).unwrap_or(1);
if vim.state().mode.is_visual() {
if vim.mode.is_visual() {
times = 1;
} else if times > 1 {
// 2J joins two lines together (same as J or 1J)
times -= 1;
}
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
}
})
});
if vim.state().mode.is_visual() {
if vim.mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &Indent, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
for _ in 0..count {
@ -167,17 +147,15 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
if vim.mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &Outdent, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
for _ in 0..count {
@ -186,103 +164,96 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
if vim.mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Undo, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &Undo, cx| {
let times = vim.take_count(cx);
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.undo(&editor::actions::Undo, cx);
}
});
})
});
workspace.register_action(|_: &mut Workspace, _: &Redo, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &Redo, cx| {
let times = vim.take_count(cx);
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.redo(&editor::actions::Redo, cx);
}
});
})
});
paste::register(workspace, cx);
repeat::register(workspace, cx);
scroll::register(workspace, cx);
search::register(workspace, cx);
substitute::register(workspace, cx);
increment::register(workspace, cx);
repeat::register(editor, cx);
scroll::register(editor, cx);
search::register(editor, cx);
substitute::register(editor, cx);
increment::register(editor, cx);
}
impl Vim {
pub fn normal_motion(
&mut self,
motion: Motion,
operator: Option<Operator>,
times: Option<usize>,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
Vim::update(cx, |vim, cx| {
match operator {
None => move_cursor(vim, motion, times, cx),
Some(Operator::Change) => change_motion(vim, motion, times, cx),
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
None => self.move_cursor(motion, times, cx),
Some(Operator::Change) => self.change_motion(motion, times, cx),
Some(Operator::Delete) => self.delete_motion(motion, times, cx),
Some(Operator::Yank) => self.yank_motion(motion, times, cx),
Some(Operator::AddSurrounds { target: None }) => {}
Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx),
Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx),
Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx),
Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx),
Some(Operator::Lowercase) => {
change_case_motion(vim, motion, times, CaseTarget::Lowercase, cx)
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
}
Some(Operator::Uppercase) => {
change_case_motion(vim, motion, times, CaseTarget::Uppercase, cx)
self.change_case_motion(motion, times, CaseTarget::Uppercase, cx)
}
Some(Operator::OppositeCase) => {
change_case_motion(vim, motion, times, CaseTarget::OppositeCase, cx)
self.change_case_motion(motion, times, CaseTarget::OppositeCase, cx)
}
Some(Operator::ToggleComments) => toggle_comments_motion(vim, motion, times, cx),
Some(Operator::ToggleComments) => self.toggle_comments_motion(motion, times, cx),
Some(operator) => {
// Can't do anything for text objects, Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
});
}
pub fn normal_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
let mut waiting_operator: Option<Operator> = None;
match vim.maybe_pop_operator() {
Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
Some(Operator::Change) => change_object(vim, object, around, cx),
Some(Operator::Delete) => delete_object(vim, object, around, cx),
Some(Operator::Yank) => yank_object(vim, object, around, cx),
match self.maybe_pop_operator() {
Some(Operator::Object { around }) => match self.maybe_pop_operator() {
Some(Operator::Change) => self.change_object(object, around, cx),
Some(Operator::Delete) => self.delete_object(object, around, cx),
Some(Operator::Yank) => self.yank_object(object, around, cx),
Some(Operator::Indent) => {
indent_object(vim, object, around, IndentDirection::In, cx)
self.indent_object(object, around, IndentDirection::In, cx)
}
Some(Operator::Outdent) => {
indent_object(vim, object, around, IndentDirection::Out, cx)
self.indent_object(object, around, IndentDirection::Out, cx)
}
Some(Operator::Lowercase) => {
change_case_object(vim, object, around, CaseTarget::Lowercase, cx)
self.change_case_object(object, around, CaseTarget::Lowercase, cx)
}
Some(Operator::Uppercase) => {
change_case_object(vim, object, around, CaseTarget::Uppercase, cx)
self.change_case_object(object, around, CaseTarget::Uppercase, cx)
}
Some(Operator::OppositeCase) => {
change_case_object(vim, object, around, CaseTarget::OppositeCase, cx)
self.change_case_object(object, around, CaseTarget::OppositeCase, cx)
}
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object)),
});
}
Some(Operator::ToggleComments) => toggle_comments_object(vim, object, around, cx),
Some(Operator::ToggleComments) => self.toggle_comments_object(object, around, cx),
_ => {
// Can't do anything for namespace operators. Ignoring
}
@ -291,7 +262,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
waiting_operator = Some(Operator::DeleteSurrounds);
}
Some(Operator::ChangeSurrounds { target: None }) => {
if check_and_move_to_valid_bracket_pair(vim, object, cx) {
if self.check_and_move_to_valid_bracket_pair(object, cx) {
waiting_operator = Some(Operator::ChangeSurrounds {
target: Some(object),
});
@ -301,20 +272,19 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
// Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
}
}
vim.clear_operator(cx);
self.clear_operator(cx);
if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
self.push_operator(operator, cx);
}
});
}
pub(crate) fn move_cursor(
vim: &mut Vim,
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
@ -326,34 +296,29 @@ pub(crate) fn move_cursor(
});
}
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
fn insert_after(&mut self, _: &InsertAfter, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
});
});
}
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
});
fn insert_before(&mut self, _: &InsertBefore, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
}
fn insert_first_non_whitespace(
_: &mut Workspace,
&mut self,
_: &InsertFirstNonWhitespace,
cx: &mut ViewContext<Workspace>,
cx: &mut ViewContext<Self>,
) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
@ -363,42 +328,36 @@ fn insert_first_non_whitespace(
});
});
});
});
}
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
fn insert_end_of_line(&mut self, _: &InsertEndOfLine, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
});
});
});
});
}
fn insert_at_previous(_: &mut Workspace, _: &InsertAtPrevious, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |vim, editor, cx| {
if let Some(marks) = vim.state().marks.get("^") {
fn insert_at_previous(&mut self, _: &InsertAtPrevious, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |vim, editor, cx| {
if let Some(marks) = vim.marks.get("^") {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
});
}
});
});
}
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
fn insert_line_above(&mut self, _: &InsertLineAbove, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@ -425,14 +384,12 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
});
});
});
});
}
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
fn insert_line_below(&mut self, _: &InsertLineBelow, cx: &mut ViewContext<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, cx);
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
@ -464,79 +421,43 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
editor.edit_with_autoindent(edits, cx);
});
});
});
}
fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx);
yank_motion(vim, motion::Motion::CurrentLine, count, cx)
})
fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx);
self.yank_motion(motion::Motion::CurrentLine, count, cx)
}
fn yank_to_end_of_line(_: &mut Workspace, _: &YankToEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx);
yank_motion(
vim,
fn yank_to_end_of_line(&mut self, _: &YankToEndOfLine, cx: &mut ViewContext<Self>) {
self.record_current_action(cx);
let count = self.take_count(cx);
self.yank_motion(
motion::Motion::EndOfLine {
display_lines: false,
},
count,
cx,
)
})
}
fn toggle_comments(_: &mut Workspace, _: &ToggleComments, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |_, editor, cx| {
fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
self.record_current_action(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
editor.toggle_comments(&Default::default(), cx);
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
if self.mode.is_visual() {
self.switch_mode(Mode::Normal, false, cx)
}
});
}
fn save_selection_starts(editor: &Editor, cx: &mut ViewContext<Editor>) -> HashMap<usize, Anchor> {
let (map, selections) = editor.selections.all_display(cx);
selections
.iter()
.map(|selection| {
(
selection.id,
map.display_point_to_anchor(selection.start, Bias::Right),
)
})
.collect::<HashMap<_, _>>()
}
fn restore_selection_cursors(
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
positions: &mut HashMap<usize, Anchor>,
) {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
}
});
});
}
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx).unwrap_or(1);
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
pub(crate) fn normal_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx).unwrap_or(1);
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (map, display_selections) = editor.selections.all_display(cx);
@ -571,10 +492,36 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
});
});
});
vim.pop_operator(cx)
});
self.pop_operator(cx);
}
}
fn save_selection_starts(editor: &Editor, cx: &mut ViewContext<Editor>) -> HashMap<usize, Anchor> {
let (map, selections) = editor.selections.all_display(cx);
selections
.iter()
.map(|selection| {
(
selection.id,
map.display_point_to_anchor(selection.start, Bias::Right),
)
})
.collect::<HashMap<_, _>>()
}
fn restore_selection_cursors(
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
positions: &mut HashMap<usize, Anchor>,
) {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
}
});
});
}
#[cfg(test)]
mod test {
use gpui::{KeyBinding, TestAppContext};

View File

@ -3,8 +3,6 @@ use editor::{display_map::ToDisplayPoint, scroll::Autoscroll};
use gpui::ViewContext;
use language::{Bias, Point, SelectionGoal};
use multi_buffer::MultiBufferRow;
use ui::WindowContext;
use workspace::Workspace;
use crate::{
motion::Motion,
@ -20,15 +18,16 @@ pub enum CaseTarget {
OppositeCase,
}
impl Vim {
pub fn change_case_motion(
vim: &mut Vim,
&mut self,
motion: Motion,
times: Option<usize>,
mode: CaseTarget,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -57,14 +56,14 @@ pub fn change_case_motion(
}
pub fn change_case_object(
vim: &mut Vim,
&mut self,
object: Object,
around: bool,
mode: CaseTarget,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
@ -93,8 +92,8 @@ pub fn change_case_object(
});
}
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
manipulate_text(cx, |c| {
pub fn change_case(&mut self, _: &ChangeCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |c| {
if c.is_lowercase() {
c.to_uppercase().collect::<Vec<char>>()
} else {
@ -103,37 +102,28 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
})
}
pub fn convert_to_upper_case(
_: &mut Workspace,
_: &ConvertToUpperCase,
cx: &mut ViewContext<Workspace>,
) {
manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
}
pub fn convert_to_lower_case(
_: &mut Workspace,
_: &ConvertToLowerCase,
cx: &mut ViewContext<Workspace>,
) {
manipulate_text(cx, |c| c.to_lowercase().collect::<Vec<char>>())
pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |c| c.to_lowercase().collect::<Vec<char>>())
}
fn manipulate_text<F>(cx: &mut ViewContext<Workspace>, transform: F)
fn manipulate_text<F>(&mut self, cx: &mut ViewContext<Self>, transform: F)
where
F: Fn(char) -> Vec<char> + Copy,
{
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.store_visual_marks(cx);
let count = vim.take_count(cx).unwrap_or(1) as u32;
self.record_current_action(cx);
self.store_visual_marks(cx);
let count = self.take_count(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |vim, editor, cx| {
self.update_editor(cx, |vim, editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) {
match vim.state().mode {
match vim.mode {
Mode::VisualLine => {
let start = Point::new(selection.start.row, 0);
let end = Point::new(
@ -186,8 +176,8 @@ where
})
});
});
vim.switch_mode(Mode::Normal, true, cx)
})
self.switch_mode(Mode::Normal, true, cx)
}
}
#[cfg(test)]

View File

@ -1,6 +1,5 @@
use crate::{
motion::{self, Motion},
normal::yank::copy_selections_content,
object::Object,
state::Mode,
Vim,
@ -11,10 +10,16 @@ use editor::{
scroll::Autoscroll,
Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::{char_kind, CharKind, Selection};
use ui::ViewContext;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
impl Vim {
pub fn change_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
@ -24,7 +29,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
| Motion::Backspace
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |vim, editor, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
@ -52,12 +57,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
&text_layout_details,
);
if let Motion::CurrentLine = motion {
let mut start_offset = selection.start.to_offset(map, Bias::Left);
let mut start_offset =
selection.start.to_offset(map, Bias::Left);
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(&map));
for (ch, offset) in map.buffer_chars_at(start_offset) {
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
if ch == '\n'
|| char_kind(&scope, ch) != CharKind::Whitespace
{
break;
}
start_offset = offset + ch.len_utf8();
@ -69,21 +77,21 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
}
});
});
copy_selections_content(vim, editor, motion.linewise(), cx);
vim.copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
if motion_succeeded {
vim.switch_mode(Mode::Insert, false, cx)
self.switch_mode(Mode::Insert, false, cx)
} else {
vim.switch_mode(Mode::Normal, false, cx)
self.switch_mode(Mode::Normal, false, cx)
}
}
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
pub fn change_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
let mut objects_found = false;
vim.update_active_editor(cx, |vim, editor, cx| {
self.update_editor(cx, |vim, editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
@ -93,16 +101,17 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
});
});
if objects_found {
copy_selections_content(vim, editor, false, cx);
vim.copy_selections_content(editor, false, cx);
editor.insert("", cx);
}
});
});
if objects_found {
vim.switch_mode(Mode::Insert, false, cx);
self.switch_mode(Mode::Insert, false, cx);
} else {
vim.switch_mode(Mode::Normal, false, cx);
self.switch_mode(Mode::Normal, false, cx);
}
}
}

View File

@ -1,17 +1,23 @@
use crate::{motion::Motion, normal::yank::copy_selections_content, object::Object, Vim};
use crate::{motion::Motion, object::Object, Vim};
use collections::{HashMap, HashSet};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
scroll::Autoscroll,
Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::{Point, Selection};
use multi_buffer::MultiBufferRow;
use ui::ViewContext;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
pub fn delete_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.stop_recording(cx);
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -44,7 +50,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
}
});
});
copy_selections_content(vim, editor, motion.linewise(), cx);
vim.copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
@ -65,9 +71,9 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
});
}
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |vim, editor, cx| {
pub fn delete_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
self.stop_recording(cx);
self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
// Emulates behavior in vim where if we expanded backwards to include a newline
@ -82,7 +88,8 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
let start = selection.start.to_offset(map, Bias::Left);
if selection.start.row().0 > 0 {
should_move_to_start.insert(selection.id);
selection.start = (start - '\n'.len_utf8()).to_display_point(map);
selection.start =
(start - '\n'.len_utf8()).to_display_point(map);
}
};
let range = selection.start.to_offset(map, Bias::Left)
@ -124,7 +131,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
}
});
});
copy_selections_content(vim, editor, false, cx);
vim.copy_selections_content(editor, false, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
@ -142,6 +149,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
});
});
}
}
fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
let end = selection.end.to_offset(map, Bias::Left);

View File

@ -1,10 +1,9 @@
use std::ops::Range;
use editor::{scroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint};
use gpui::{impl_actions, ViewContext, WindowContext};
use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint};
use gpui::{impl_actions, ViewContext};
use language::{Bias, Point};
use serde::Deserialize;
use workspace::Workspace;
use crate::{state::Mode, Vim};
@ -24,35 +23,32 @@ struct Decrement {
impl_actions!(vim, [Increment, Decrement]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, action: &Increment, cx| {
Vim::update(cx, |vim, cx| {
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, action: &Increment, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
let step = if action.step { 1 } else { 0 };
increment(vim, count as i32, step, cx)
})
vim.increment(count as i32, step, cx)
});
workspace.register_action(|_: &mut Workspace, action: &Decrement, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, action: &Decrement, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
let step = if action.step { -1 } else { 0 };
increment(vim, count as i32 * -1, step, cx)
})
vim.increment(count as i32 * -1, step, cx)
});
}
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
vim.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
fn increment(&mut self, mut delta: i32, step: i32, cx: &mut ViewContext<Self>) {
self.store_visual_marks(cx);
self.update_editor(cx, |vim, editor, cx| {
let mut edits = Vec::new();
let mut new_anchors = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all_adjusted(cx) {
if !selection.is_empty() {
if vim.state().mode != Mode::VisualBlock || new_anchors.is_empty() {
if vim.mode != Mode::VisualBlock || new_anchors.is_empty() {
new_anchors.push((true, snapshot.anchor_before(selection.start)))
}
}
@ -109,7 +105,8 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
})
});
});
vim.switch_mode(Mode::Normal, true, cx)
self.switch_mode(Mode::Normal, true, cx)
}
}
fn find_number(

View File

@ -1,24 +1,25 @@
use crate::{motion::Motion, object::Object, Vim};
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Bias};
use gpui::WindowContext;
use language::SelectionGoal;
use ui::ViewContext;
#[derive(PartialEq, Eq)]
pub(super) enum IndentDirection {
pub(crate) enum IndentDirection {
In,
Out,
}
pub fn indent_motion(
vim: &mut Vim,
impl Vim {
pub(crate) fn indent_motion(
&mut self,
motion: Motion,
times: Option<usize>,
dir: IndentDirection,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -44,15 +45,15 @@ pub fn indent_motion(
});
}
pub fn indent_object(
vim: &mut Vim,
pub(crate) fn indent_object(
&mut self,
object: Object,
around: bool,
dir: IndentDirection,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
@ -76,3 +77,4 @@ pub fn indent_object(
});
});
}
}

View File

@ -6,7 +6,7 @@ use editor::{
scroll::Autoscroll,
Anchor, Bias, DisplayPoint,
};
use gpui::WindowContext;
use gpui::ViewContext;
use language::SelectionGoal;
use crate::{
@ -15,8 +15,9 @@ use crate::{
Vim,
};
pub fn create_mark(vim: &mut Vim, text: Arc<str>, tail: bool, cx: &mut WindowContext) {
let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| {
impl Vim {
pub fn create_mark(&mut self, text: Arc<str>, tail: bool, cx: &mut ViewContext<Self>) {
let Some(anchors) = self.update_editor(cx, |_, editor, _| {
editor
.selections
.disjoint_anchors()
@ -26,16 +27,24 @@ pub fn create_mark(vim: &mut Vim, text: Arc<str>, tail: bool, cx: &mut WindowCon
}) else {
return;
};
vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
vim.clear_operator(cx);
self.marks.insert(text.to_string(), anchors);
self.clear_operator(cx);
}
pub fn create_visual_marks(vim: &mut Vim, mode: Mode, cx: &mut WindowContext) {
// When handling an action, you must create visual marks if you will switch to normal
// mode without the default selection behavior.
pub(crate) fn store_visual_marks(&mut self, cx: &mut ViewContext<Self>) {
if self.mode.is_visual() {
self.create_visual_marks(self.mode, cx);
}
}
pub(crate) fn create_visual_marks(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let mut starts = vec![];
let mut ends = vec![];
let mut reversed = vec![];
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
for selection in selections {
let end = movement::saturating_left(&map, selection.end);
@ -51,20 +60,17 @@ pub fn create_visual_marks(vim: &mut Vim, mode: Mode, cx: &mut WindowContext) {
}
});
vim.update_state(|state| {
state.marks.insert("<".to_string(), starts);
state.marks.insert(">".to_string(), ends);
state.stored_visual_mode.replace((mode, reversed));
});
vim.clear_operator(cx);
self.marks.insert("<".to_string(), starts);
self.marks.insert(">".to_string(), ends);
self.stored_visual_mode.replace((mode, reversed));
self.clear_operator(cx);
}
pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
let anchors = Vim::update(cx, |vim, cx| {
vim.pop_operator(cx);
pub fn jump(&mut self, text: Arc<str>, line: bool, cx: &mut ViewContext<Self>) {
self.pop_operator(cx);
match &*text {
"{" | "}" => vim.update_active_editor(cx, |_, editor, cx| {
let anchors = match &*text {
"{" | "}" => self.update_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
@ -79,17 +85,16 @@ pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
})
.collect::<Vec<Anchor>>()
}),
"." => vim.state().change_list.last().cloned(),
_ => vim.state().marks.get(&*text).cloned(),
}
});
"." => self.change_list.last().cloned(),
_ => self.marks.get(&*text).cloned(),
};
let Some(anchors) = anchors else { return };
let is_active_operator = Vim::read(cx).state().active_operator().is_some();
let is_active_operator = self.active_operator().is_some();
if is_active_operator {
if let Some(anchor) = anchors.last() {
motion::motion(
self.motion(
Motion::Jump {
anchor: *anchor,
line,
@ -99,8 +104,7 @@ pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
}
return;
} else {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
let map = editor.snapshot(cx);
let mut ranges: Vec<Range<Anchor>> = Vec::new();
for mut anchor in anchors {
@ -120,7 +124,7 @@ pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
s.select_anchor_ranges(ranges)
})
});
})
}
}
}

View File

@ -4,17 +4,15 @@ use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayP
use gpui::{impl_actions, ViewContext};
use language::{Bias, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
use crate::{
normal::yank::copy_selections_content,
state::{Mode, Register},
Vim,
};
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Paste {
pub struct Paste {
#[serde(default)]
before: bool,
#[serde(default)]
@ -23,37 +21,34 @@ struct Paste {
impl_actions!(vim, [Paste]);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(paste);
}
impl Vim {
pub fn paste(&mut self, action: &Paste, cx: &mut ViewContext<Self>) {
self.record_current_action(cx);
self.store_visual_marks(cx);
let count = self.take_count(cx).unwrap_or(1);
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.store_visual_marks(cx);
let count = vim.take_count(cx).unwrap_or(1);
vim.update_active_editor(cx, |vim, editor, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let selected_register = vim.update_state(|state| state.selected_register.take());
let selected_register = vim.selected_register.take();
let Some(Register {
text,
clipboard_selections,
}) = vim
.read_register(selected_register, Some(editor), cx)
}) = Vim::update_globals(cx, |globals, cx| {
globals.read_register(selected_register, Some(editor), cx)
})
.filter(|reg| !reg.text.is_empty())
else {
return;
};
let clipboard_selections = clipboard_selections
.filter(|sel| sel.len() > 1 && vim.state().mode != Mode::VisualLine);
.filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
if !action.preserve_clipboard && vim.state().mode.is_visual() {
copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
if !action.preserve_clipboard && vim.mode.is_visual() {
vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, cx);
}
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
@ -90,7 +85,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
.first()
.map(|selection| selection.first_line_indent)
});
let before = action.before || vim.state().mode == Mode::VisualLine;
let before = action.before || vim.mode == Mode::VisualLine;
let mut edits = Vec::new();
let mut new_selections = Vec::new();
@ -121,7 +116,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
} else {
to_insert = "\n".to_owned() + &to_insert;
}
} else if !line_mode && vim.state().mode == Mode::VisualLine {
} else if !line_mode && vim.mode == Mode::VisualLine {
to_insert = to_insert + "\n";
}
@ -145,7 +140,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
let point_range = display_range.start.to_point(&display_map)
..display_range.end.to_point(&display_map);
let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
let anchor = if is_multiline || vim.mode == Mode::VisualLine {
display_map.buffer_snapshot.anchor_before(point_range.start)
} else {
display_map.buffer_snapshot.anchor_after(point_range.end)
@ -185,7 +180,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
cursor = movement::saturating_left(map, cursor)
}
cursors.push(cursor);
if vim.state().mode == Mode::VisualBlock {
if vim.mode == Mode::VisualBlock {
break;
}
}
@ -195,8 +190,8 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
})
});
});
vim.switch_mode(Mode::Normal, true, cx);
});
self.switch_mode(Mode::Normal, true, cx);
}
}
#[cfg(test)]

View File

@ -1,12 +1,12 @@
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
use std::{cell::RefCell, rc::Rc};
use crate::{
insert::NormalBefore,
motion::Motion,
state::{Mode, Operator, RecordedSelection, ReplayableAction},
visual::visual_motion,
state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
Vim,
};
use editor::Editor;
use gpui::{actions, Action, ViewContext, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@ -44,30 +44,28 @@ fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
}
}
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| {
Vim::update(cx, |vim, cx| {
vim.workspace_state.dot_replaying = false;
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &EndRepeat, cx| {
Vim::globals(cx).dot_replaying = false;
vim.switch_mode(Mode::Normal, false, cx)
});
});
workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
workspace.register_action(|_: &mut Workspace, _: &ToggleRecord, cx| {
Vim::update(cx, |vim, cx| {
if let Some(char) = vim.workspace_state.recording_register.take() {
vim.workspace_state.last_recorded_register = Some(char)
Vim::action(editor, cx, |vim, _: &Repeat, cx| vim.repeat(false, cx));
Vim::action(editor, cx, |vim, _: &ToggleRecord, cx| {
let globals = Vim::globals(cx);
if let Some(char) = globals.recording_register.take() {
globals.last_recorded_register = Some(char)
} else {
vim.push_operator(Operator::RecordRegister, cx);
}
})
});
workspace.register_action(|_: &mut Workspace, _: &ReplayLastRecording, cx| {
let Some(register) = Vim::read(cx).workspace_state.last_recorded_register else {
Vim::action(editor, cx, |vim, _: &ReplayLastRecording, cx| {
let Some(register) = Vim::globals(cx).last_recorded_register else {
return;
};
replay_register(register, cx)
vim.replay_register(register, cx)
});
}
@ -116,54 +114,60 @@ impl Replayer {
lock.ix += 1;
drop(lock);
let Some(action) = action else {
Vim::update(cx, |vim, _| vim.workspace_state.replayer.take());
Vim::globals(cx).replayer.take();
return;
};
match action {
ReplayableAction::Action(action) => {
if should_replay(&*action) {
cx.dispatch_action(action.boxed_clone());
cx.defer(move |cx| observe_action(action.boxed_clone(), cx));
cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
}
}
ReplayableAction::Insertion {
text,
utf16_range_to_replace,
} => {
if let Some(editor) = Vim::read(cx).active_editor.clone() {
editor
.update(cx, |editor, cx| {
cx.window_handle()
.update(cx, |handle, cx| {
let Ok(workspace) = handle.downcast::<Workspace>() else {
return;
};
let Some(editor) = workspace.read(cx).active_item_as::<Editor>(cx) else {
return;
};
editor.update(cx, |editor, cx| {
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
})
})
.log_err();
}
}
}
cx.defer(move |cx| self.next(cx));
}
}
pub(crate) fn record_register(register: char, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.workspace_state.recording_register = Some(register);
vim.workspace_state.recordings.remove(&register);
vim.workspace_state.ignore_current_insertion = true;
vim.clear_operator(cx)
})
impl Vim {
pub(crate) fn record_register(&mut self, register: char, cx: &mut ViewContext<Self>) {
let globals = Vim::globals(cx);
globals.recording_register = Some(register);
globals.recordings.remove(&register);
globals.ignore_current_insertion = true;
self.clear_operator(cx)
}
pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
let mut count = vim.take_count(cx).unwrap_or(1);
vim.clear_operator(cx);
pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext<Self>) {
let mut count = self.take_count(cx).unwrap_or(1);
self.clear_operator(cx);
let globals = Vim::globals(cx);
if register == '@' {
let Some(last) = vim.workspace_state.last_replayed_register else {
let Some(last) = globals.last_replayed_register else {
return;
};
register = last;
}
let Some(actions) = vim.workspace_state.recordings.get(&register) else {
let Some(actions) = globals.recordings.get(&register) else {
return;
};
@ -173,48 +177,24 @@ pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) {
count -= 1
}
vim.workspace_state.last_replayed_register = Some(register);
vim.workspace_state
globals.last_replayed_register = Some(register);
let mut replayer = globals
.replayer
.get_or_insert_with(|| Replayer::new())
.replay(repeated_actions, cx);
});
.clone();
replayer.replay(repeated_actions, cx);
}
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
let Some((mut actions, selection)) = Vim::update(cx, |vim, cx| {
let actions = vim.workspace_state.recorded_actions.clone();
pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx);
let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
let actions = globals.recorded_actions.clone();
if actions.is_empty() {
return None;
}
let count = vim.take_count(cx);
let selection = vim.workspace_state.recorded_selection.clone();
match selection {
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::Visual, false, cx)
}
RecordedSelection::VisualLine { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::VisualLine, false, cx)
}
RecordedSelection::VisualBlock { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::VisualBlock, false, cx)
}
RecordedSelection::None => {
if let Some(count) = count {
vim.workspace_state.recorded_count = Some(count);
}
}
}
if vim.workspace_state.replayer.is_none() {
if let Some(recording_register) = vim.workspace_state.recording_register {
vim.workspace_state
if globals.replayer.is_none() {
if let Some(recording_register) = globals.recording_register {
globals
.recordings
.entry(recording_register)
.or_default()
@ -222,26 +202,51 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
}
}
Some((actions, selection))
let mut mode = None;
let selection = globals.recorded_selection.clone();
match selection {
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
globals.recorded_count = None;
mode = Some(Mode::Visual);
}
RecordedSelection::VisualLine { .. } => {
globals.recorded_count = None;
mode = Some(Mode::VisualLine)
}
RecordedSelection::VisualBlock { .. } => {
globals.recorded_count = None;
mode = Some(Mode::VisualBlock)
}
RecordedSelection::None => {
if let Some(count) = count {
globals.recorded_count = Some(count);
}
}
}
Some((actions, selection, mode))
}) else {
return;
};
if let Some(mode) = mode {
self.switch_mode(mode, false, cx)
}
match selection {
RecordedSelection::SingleLine { cols } => {
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
}
}
RecordedSelection::Visual { rows, cols } => {
visual_motion(
self.visual_motion(
Motion::Down {
display_lines: false,
},
Some(rows as usize),
cx,
);
visual_motion(
self.visual_motion(
Motion::StartOfLine {
display_lines: false,
},
@ -249,11 +254,11 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
cx,
);
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
}
}
RecordedSelection::VisualBlock { rows, cols } => {
visual_motion(
self.visual_motion(
Motion::Down {
display_lines: false,
},
@ -261,11 +266,11 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
cx,
);
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
self.visual_motion(Motion::Right, Some(cols as usize - 1), cx);
}
}
RecordedSelection::VisualLine { rows } => {
visual_motion(
self.visual_motion(
Motion::Down {
display_lines: false,
},
@ -289,7 +294,7 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
let mut new_actions = actions.clone();
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
// if we came from insert mode we're just doing repetitions 2 onwards.
if from_insert_mode {
@ -306,73 +311,14 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
Vim::update(cx, |vim, cx| {
vim.workspace_state.dot_replaying = true;
vim.workspace_state
let globals = Vim::globals(cx);
globals.dot_replaying = true;
let mut replayer = globals
.replayer
.get_or_insert_with(|| Replayer::new())
.replay(actions, cx);
})
.clone();
replayer.replay(actions, cx);
}
pub(crate) fn observe_action(action: Box<dyn Action>, cx: &mut WindowContext) {
Vim::update(cx, |vim, _| {
if vim.workspace_state.dot_recording {
vim.workspace_state
.recorded_actions
.push(ReplayableAction::Action(action.boxed_clone()));
if vim.workspace_state.stop_recording_after_next_action {
vim.workspace_state.dot_recording = false;
vim.workspace_state.stop_recording_after_next_action = false;
}
}
if vim.workspace_state.replayer.is_none() {
if let Some(recording_register) = vim.workspace_state.recording_register {
vim.workspace_state
.recordings
.entry(recording_register)
.or_default()
.push(ReplayableAction::Action(action));
}
}
})
}
pub(crate) fn observe_insertion(
text: &Arc<str>,
range_to_replace: Option<Range<isize>>,
cx: &mut WindowContext,
) {
Vim::update(cx, |vim, _| {
if vim.workspace_state.ignore_current_insertion {
vim.workspace_state.ignore_current_insertion = false;
return;
}
if vim.workspace_state.dot_recording {
vim.workspace_state
.recorded_actions
.push(ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace.clone(),
});
if vim.workspace_state.stop_recording_after_next_action {
vim.workspace_state.dot_recording = false;
vim.workspace_state.stop_recording_after_next_action = false;
}
}
if let Some(recording_register) = vim.workspace_state.recording_register {
vim.workspace_state
.recordings
.entry(recording_register)
.or_default()
.push(ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace,
});
}
});
}
#[cfg(test)]

View File

@ -7,28 +7,27 @@ use editor::{
use gpui::{actions, ViewContext};
use language::Bias;
use settings::Settings;
use workspace::Workspace;
actions!(
vim,
[LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown]
);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, _: &LineDown, cx| {
scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &LineDown, cx| {
vim.scroll(false, cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
});
workspace.register_action(|_: &mut Workspace, _: &LineUp, cx| {
scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
Vim::action(editor, cx, |vim, _: &LineUp, cx| {
vim.scroll(false, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
});
workspace.register_action(|_: &mut Workspace, _: &PageDown, cx| {
scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
Vim::action(editor, cx, |vim, _: &PageDown, cx| {
vim.scroll(false, cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
});
workspace.register_action(|_: &mut Workspace, _: &PageUp, cx| {
scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
Vim::action(editor, cx, |vim, _: &PageUp, cx| {
vim.scroll(false, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
});
workspace.register_action(|_: &mut Workspace, _: &ScrollDown, cx| {
scroll(cx, true, |c| {
Vim::action(editor, cx, |vim, _: &ScrollDown, cx| {
vim.scroll(true, cx, |c| {
if let Some(c) = c {
ScrollAmount::Line(c)
} else {
@ -36,8 +35,8 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
}
})
});
workspace.register_action(|_: &mut Workspace, _: &ScrollUp, cx| {
scroll(cx, true, |c| {
Vim::action(editor, cx, |vim, _: &ScrollUp, cx| {
vim.scroll(true, cx, |c| {
if let Some(c) = c {
ScrollAmount::Line(-c)
} else {
@ -47,17 +46,18 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
});
}
impl Vim {
fn scroll(
cx: &mut ViewContext<Workspace>,
&mut self,
move_cursor: bool,
cx: &mut ViewContext<Self>,
by: fn(c: Option<f32>) -> ScrollAmount,
) {
Vim::update(cx, |vim, cx| {
let amount = by(vim.take_count(cx).map(|c| c as f32));
vim.update_active_editor(cx, |_, editor, cx| {
let amount = by(self.take_count(cx).map(|c| c as f32));
self.update_editor(cx, |_, editor, cx| {
scroll_editor(editor, move_cursor, &amount, cx)
});
})
}
}
fn scroll_editor(

View File

@ -1,15 +1,15 @@
use std::{iter::Peekable, str::Chars, time::Duration};
use editor::Editor;
use gpui::{actions, impl_actions, ViewContext};
use language::Point;
use search::{buffer_search, BufferSearchBar, SearchOptions};
use serde_derive::Deserialize;
use workspace::{notifications::NotifyResultExt, searchable::Direction, Workspace};
use workspace::{notifications::NotifyResultExt, searchable::Direction};
use crate::{
command::CommandRange,
motion::{search_motion, Motion},
normal::move_cursor,
motion::Motion,
state::{Mode, SearchState},
Vim,
};
@ -60,53 +60,43 @@ impl_actions!(
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(move_to_next);
workspace.register_action(move_to_prev);
workspace.register_action(move_to_next_match);
workspace.register_action(move_to_prev_match);
workspace.register_action(search);
workspace.register_action(search_submit);
workspace.register_action(search_deploy);
workspace.register_action(find_command);
workspace.register_action(replace_command);
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, Vim::move_to_next);
Vim::action(editor, cx, Vim::move_to_prev);
Vim::action(editor, cx, Vim::move_to_next_match);
Vim::action(editor, cx, Vim::move_to_prev_match);
Vim::action(editor, cx, Vim::search);
Vim::action(editor, cx, Vim::search_deploy);
Vim::action(editor, cx, Vim::find_command);
Vim::action(editor, cx, Vim::replace_command);
}
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
impl Vim {
fn move_to_next(&mut self, action: &MoveToNext, cx: &mut ViewContext<Self>) {
self.move_to_internal(Direction::Next, !action.partial_word, cx)
}
fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
fn move_to_prev(&mut self, action: &MoveToPrev, cx: &mut ViewContext<Self>) {
self.move_to_internal(Direction::Prev, !action.partial_word, cx)
}
fn move_to_next_match(
workspace: &mut Workspace,
_: &MoveToNextMatch,
cx: &mut ViewContext<Workspace>,
) {
move_to_match_internal(workspace, Direction::Next, cx)
fn move_to_next_match(&mut self, _: &MoveToNextMatch, cx: &mut ViewContext<Self>) {
self.move_to_match_internal(Direction::Next, cx)
}
fn move_to_prev_match(
workspace: &mut Workspace,
_: &MoveToPrevMatch,
cx: &mut ViewContext<Workspace>,
) {
move_to_match_internal(workspace, Direction::Prev, cx)
fn move_to_prev_match(&mut self, _: &MoveToPrevMatch, cx: &mut ViewContext<Self>) {
self.move_to_match_internal(Direction::Prev, cx)
}
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
fn search(&mut self, action: &Search, cx: &mut ViewContext<Self>) {
let Some(pane) = self.pane(cx) else { return };
let direction = if action.backwards {
Direction::Prev
} else {
Direction::Next
};
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx).unwrap_or(1);
let prior_selections = vim.editor_selections(cx);
let count = self.take_count(cx).unwrap_or(1);
let prior_selections = self.editor_selections(cx);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
@ -122,69 +112,65 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
search_bar.set_replacement(None, cx);
search_bar.set_search_options(SearchOptions::REGEX, cx);
}
vim.update_state(|state| {
state.search = SearchState {
self.search = SearchState {
direction,
count,
initial_query: query.clone(),
prior_selections,
prior_operator: state.operator_stack.last().cloned(),
prior_mode: state.mode,
prior_operator: self.operator_stack.last().cloned(),
prior_mode: self.mode,
}
});
});
}
})
})
}
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, _| {
vim.update_state(|state| state.search = Default::default())
});
fn search_deploy(&mut self, _: &buffer_search::Deploy, cx: &mut ViewContext<Self>) {
self.search = Default::default();
cx.propagate();
}
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
let mut motion = None;
Vim::update(cx, |vim, cx| {
vim.store_visual_marks(cx);
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
pub fn search_submit(&mut self, cx: &mut ViewContext<Self>) {
self.store_visual_marks(cx);
let Some(pane) = self.pane(cx) else { return };
let result = pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return None;
};
search_bar.update(cx, |search_bar, cx| {
let (mut prior_selections, prior_mode, prior_operator) =
vim.update_state(|state| {
let mut count = state.search.count;
let direction = state.search.direction;
let mut count = self.search.count;
let direction = self.search.direction;
// in the case that the query has changed, the search bar
// will have selected the next match already.
if (search_bar.query(cx) != state.search.initial_query)
&& state.search.direction == Direction::Next
if (search_bar.query(cx) != self.search.initial_query)
&& self.search.direction == Direction::Next
{
count = count.saturating_sub(1)
}
state.search.count = 1;
self.search.count = 1;
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx);
let prior_selections: Vec<_> =
state.search.prior_selections.drain(..).collect();
let prior_mode = state.search.prior_mode;
let prior_operator = state.search.prior_operator.take();
(prior_selections, prior_mode, prior_operator)
let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
let prior_mode = self.search.prior_mode;
let prior_operator = self.search.prior_operator.take();
let query = search_bar.query(cx).into();
Vim::globals(cx).registers.insert('/', query);
Some((prior_selections, prior_mode, prior_operator))
})
});
vim.workspace_state
.registers
.insert('/', search_bar.query(cx).into());
let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
return;
};
let new_selections = vim.editor_selections(cx);
let new_selections = self.editor_selections(cx);
// If the active editor has changed during a search, don't panic.
if prior_selections.iter().any(|s| {
vim.update_active_editor(cx, |_vim, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
!s.start.is_valid(&editor.snapshot(cx).buffer_snapshot)
})
.unwrap_or(true)
@ -192,79 +178,73 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
prior_selections.clear();
}
if prior_mode != vim.state().mode {
vim.switch_mode(prior_mode, true, cx);
if prior_mode != self.mode {
self.switch_mode(prior_mode, true, cx);
}
if let Some(operator) = prior_operator {
vim.push_operator(operator, cx);
self.push_operator(operator, cx);
};
motion = Some(Motion::ZedSearchResult {
self.search_motion(
Motion::ZedSearchResult {
prior_selections,
new_selections,
});
});
}
});
});
if let Some(motion) = motion {
search_motion(motion, cx)
}
},
cx,
);
}
pub fn move_to_match_internal(
workspace: &mut Workspace,
direction: Direction,
cx: &mut ViewContext<Workspace>,
) {
let mut motion = None;
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
let count = vim.take_count(cx).unwrap_or(1);
let prior_selections = vim.editor_selections(cx);
pub fn move_to_match_internal(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let Some(pane) = self.pane(cx) else { return };
let count = self.take_count(cx).unwrap_or(1);
let prior_selections = self.editor_selections(cx);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let success = pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return false;
};
search_bar.update(cx, |search_bar, cx| {
if !search_bar.has_active_match() || !search_bar.show(cx) {
return;
return false;
}
search_bar.select_match(direction, count, cx);
true
})
});
if !success {
return;
}
let new_selections = vim.editor_selections(cx);
motion = Some(Motion::ZedSearchResult {
let new_selections = self.editor_selections(cx);
self.search_motion(
Motion::ZedSearchResult {
prior_selections,
new_selections,
});
})
}
})
});
if let Some(motion) = motion {
search_motion(motion, cx);
}
},
cx,
);
}
pub fn move_to_internal(
workspace: &mut Workspace,
&mut self,
direction: Direction,
whole_word: bool,
cx: &mut ViewContext<Workspace>,
cx: &mut ViewContext<Self>,
) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
let count = vim.take_count(cx).unwrap_or(1);
let prior_selections = vim.editor_selections(cx);
let Some(pane) = self.pane(cx) else { return };
let count = self.take_count(cx).unwrap_or(1);
let prior_selections = self.editor_selections(cx);
let vim = cx.view().clone();
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let searched = pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return false;
};
let search = search_bar.update(cx, |search_bar, cx| {
let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
if !search_bar.show(cx) {
return None;
}
let Some(query) = search_bar.query_suggestion(cx) else {
vim.clear_operator(cx);
drop(search_bar.search("", None, cx));
return None;
};
@ -275,38 +255,41 @@ pub fn move_to_internal(
Some(search_bar.search(&query, Some(options), cx))
});
if let Some(search) = search {
let Some(search) = search else { return false };
let search_bar = search_bar.downgrade();
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(direction, count, cx);
let new_selections =
Vim::update(cx, |vim, cx| vim.editor_selections(cx));
search_motion(
vim.update(cx, |vim, cx| {
let new_selections = vim.editor_selections(cx);
vim.search_motion(
Motion::ZedSearchResult {
prior_selections,
new_selections,
},
cx,
)
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
true
});
if !searched {
self.clear_operator(cx)
}
fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
if self.mode.is_visual() {
self.switch_mode(Mode::Normal, false, cx)
}
}
fn find_command(&mut self, action: &FindCommand, cx: &mut ViewContext<Self>) {
let Some(pane) = self.pane(cx) else { return };
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
@ -343,20 +326,15 @@ fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewCo
})
}
fn replace_command(
workspace: &mut Workspace,
action: &ReplaceCommand,
cx: &mut ViewContext<Workspace>,
) {
fn replace_command(&mut self, action: &ReplaceCommand, cx: &mut ViewContext<Self>) {
let replacement = action.replacement.clone();
let pane = workspace.active_pane().clone();
let editor = Vim::read(cx)
.active_editor
.as_ref()
.and_then(|editor| editor.upgrade());
let Some(((pane, workspace), editor)) =
self.pane(cx).zip(self.workspace(cx)).zip(self.editor())
else {
return;
};
if let Some(range) = &action.range {
if let Some(result) = Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
let range = range.buffer_range(vim, editor, cx)?;
let snapshot = &editor.snapshot(cx).buffer_snapshot;
let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
@ -364,11 +342,13 @@ fn replace_command(
..snapshot.anchor_after(end_point);
editor.set_search_within_ranges(&[range], cx);
anyhow::Ok(())
})
}) {
workspace.update(cx, |workspace, cx| {
result.notify_err(workspace, cx);
})
}
}
let vim = cx.view().clone();
pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return;
@ -399,7 +379,6 @@ fn replace_command(
if replacement.should_replace_all {
search_bar.select_last_match(cx);
search_bar.replace_all(&Default::default(), cx);
if let Some(editor) = editor {
cx.spawn(|_, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(200))
@ -409,17 +388,15 @@ fn replace_command(
.ok();
})
.detach();
}
Vim::update(cx, |vim, cx| {
move_cursor(
vim,
vim.update(cx, |vim, cx| {
vim.move_cursor(
Motion::StartOfLine {
display_lines: false,
},
None,
cx,
)
})
});
}
})?;
anyhow::Ok(())
@ -427,6 +404,7 @@ fn replace_command(
.detach_and_log_err(cx);
})
}
}
impl Replacement {
// convert a vim query into something more usable by zed.
@ -697,7 +675,7 @@ mod test {
#[gpui::test]
async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, false).await;
cx.set_state("ˇone one one one", Mode::Normal);
cx.cx.set_state("ˇone one one one");
cx.simulate_keystrokes("cmd-f");
cx.run_until_parked();

View File

@ -1,36 +1,37 @@
use editor::movement;
use gpui::{actions, ViewContext, WindowContext};
use editor::{movement, Editor};
use gpui::{actions, ViewContext};
use language::Point;
use workspace::Workspace;
use crate::{motion::Motion, normal::yank::copy_selections_content, Mode, Vim};
use crate::{motion::Motion, Mode, Vim};
actions!(vim, [Substitute, SubstituteLine]);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Substitute, cx| {
vim.start_recording(cx);
let count = vim.take_count(cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
vim.substitute(count, vim.mode == Mode::VisualLine, cx);
});
workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &SubstituteLine, cx| {
vim.start_recording(cx);
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
vim.switch_mode(Mode::VisualLine, false, cx)
}
let count = vim.take_count(cx);
substitute(vim, count, true, cx)
})
vim.substitute(count, true, cx)
});
}
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
vim.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
pub fn substitute(
&mut self,
count: Option<usize>,
line_mode: bool,
cx: &mut ViewContext<Self>,
) {
self.store_visual_marks(cx);
self.update_editor(cx, |vim, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
@ -73,13 +74,14 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
}
})
});
copy_selections_content(vim, editor, line_mode, cx);
vim.copy_selections_content(editor, line_mode, cx);
let selections = editor.selections.all::<Point>(cx).into_iter();
let edits = selections.map(|selection| (selection.start..selection.end, ""));
editor.edit(edits, cx);
});
});
vim.switch_mode(Mode::Insert, true, cx);
self.switch_mode(Mode::Insert, true, cx);
}
}
#[cfg(test)]

View File

@ -1,17 +1,18 @@
use crate::{motion::Motion, object::Object, Vim};
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Bias};
use gpui::WindowContext;
use language::SelectionGoal;
use ui::ViewContext;
impl Vim {
pub fn toggle_comments_motion(
vim: &mut Vim,
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -33,9 +34,14 @@ pub fn toggle_comments_motion(
});
}
pub fn toggle_comments_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
pub fn toggle_comments_object(
&mut self,
object: Object,
around: bool,
cx: &mut ViewContext<Self>,
) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
@ -55,3 +61,4 @@ pub fn toggle_comments_object(vim: &mut Vim, object: Object, around: bool, cx: &
});
});
}
}

View File

@ -8,13 +8,20 @@ use crate::{
};
use collections::HashMap;
use editor::{ClipboardSelection, Editor};
use gpui::WindowContext;
use gpui::ViewContext;
use language::Point;
use multi_buffer::MultiBufferRow;
use ui::ViewContext;
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
struct HighlightOnYank;
impl Vim {
pub fn yank_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -26,7 +33,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
yank_selections_content(vim, editor, motion.linewise(), cx);
vim.yank_selections_content(editor, motion.linewise(), cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
@ -37,8 +44,8 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
});
}
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
@ -49,7 +56,7 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC
original_positions.insert(selection.id, original_position);
});
});
yank_selections_content(vim, editor, false, cx);
vim.yank_selections_content(editor, false, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
@ -61,27 +68,25 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC
}
pub fn yank_selections_content(
vim: &mut Vim,
&mut self,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
copy_selections_content_internal(vim, editor, linewise, true, cx);
self.copy_selections_content_internal(editor, linewise, true, cx);
}
pub fn copy_selections_content(
vim: &mut Vim,
&mut self,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
copy_selections_content_internal(vim, editor, linewise, false, cx);
self.copy_selections_content_internal(editor, linewise, false, cx);
}
struct HighlightOnYank;
fn copy_selections_content_internal(
vim: &mut Vim,
&mut self,
editor: &mut Editor,
linewise: bool,
is_yank: bool,
@ -93,22 +98,20 @@ fn copy_selections_content_internal(
let mut clipboard_selections = Vec::with_capacity(selections.len());
let mut ranges_to_highlight = Vec::new();
vim.update_state(|state| {
state.marks.insert(
self.marks.insert(
"[".to_string(),
selections
.iter()
.map(|s| buffer.anchor_before(s.start))
.collect(),
);
state.marks.insert(
self.marks.insert(
"]".to_string(),
selections
.iter()
.map(|s| buffer.anchor_after(s.end))
.collect(),
)
});
);
{
let mut is_first = true;
@ -154,8 +157,9 @@ fn copy_selections_content_internal(
}
}
let selected_register = vim.update_state(|state| state.selected_register.take());
vim.write_registers(
let selected_register = self.selected_register.take();
Vim::update_globals(cx, |globals, cx| {
globals.write_registers(
Register {
text: text.into(),
clipboard_selections: Some(clipboard_selections),
@ -164,9 +168,10 @@ fn copy_selections_content_internal(
is_yank,
linewise,
cx,
);
)
});
if !is_yank || vim.state().mode == Mode::Visual {
if !is_yank || self.mode == Mode::Visual {
return;
}
@ -186,3 +191,4 @@ fn copy_selections_content_internal(
})
.detach();
}
}

View File

@ -2,24 +2,21 @@ use std::ops::Range;
use crate::{
motion::{coerce_punctuation, right},
normal::normal_object,
state::Mode,
visual::visual_object,
Vim,
};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange},
Bias, DisplayPoint,
Bias, DisplayPoint, Editor,
};
use itertools::Itertools;
use gpui::{actions, impl_actions, ViewContext, WindowContext};
use gpui::{actions, impl_actions, ViewContext};
use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use workspace::Workspace;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Object {
@ -65,51 +62,61 @@ actions!(
]
);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(
|_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
object(Object::Word { ignore_punctuation }, cx)
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(
editor,
cx,
|vim, &Word { ignore_punctuation }: &Word, cx| {
vim.object(Object::Word { ignore_punctuation }, cx)
},
);
workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx));
workspace
.register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
workspace
.register_action(|_: &mut Workspace, _: &Paragraph, cx: _| object(Object::Paragraph, cx));
workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
workspace
.register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
workspace.register_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| {
object(Object::DoubleQuotes, cx)
Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
Vim::action(editor, cx, |vim, _: &Sentence, cx| {
vim.object(Object::Sentence, cx)
});
workspace.register_action(|_: &mut Workspace, _: &Parentheses, cx: _| {
object(Object::Parentheses, cx)
Vim::action(editor, cx, |vim, _: &Paragraph, cx| {
vim.object(Object::Paragraph, cx)
});
workspace.register_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
object(Object::SquareBrackets, cx)
Vim::action(editor, cx, |vim, _: &Quotes, cx| {
vim.object(Object::Quotes, cx)
});
workspace.register_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| {
object(Object::CurlyBrackets, cx)
Vim::action(editor, cx, |vim, _: &BackQuotes, cx| {
vim.object(Object::BackQuotes, cx)
});
workspace.register_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| {
object(Object::AngleBrackets, cx)
Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| {
vim.object(Object::DoubleQuotes, cx)
});
workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
object(Object::VerticalBars, cx)
Vim::action(editor, cx, |vim, _: &Parentheses, cx| {
vim.object(Object::Parentheses, cx)
});
Vim::action(editor, cx, |vim, _: &SquareBrackets, cx| {
vim.object(Object::SquareBrackets, cx)
});
Vim::action(editor, cx, |vim, _: &CurlyBrackets, cx| {
vim.object(Object::CurlyBrackets, cx)
});
Vim::action(editor, cx, |vim, _: &AngleBrackets, cx| {
vim.object(Object::AngleBrackets, cx)
});
Vim::action(editor, cx, |vim, _: &VerticalBars, cx| {
vim.object(Object::VerticalBars, cx)
});
Vim::action(editor, cx, |vim, _: &Argument, cx| {
vim.object(Object::Argument, cx)
});
workspace
.register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx));
}
fn object(object: Object, cx: &mut WindowContext) {
match Vim::read(cx).state().mode {
Mode::Normal => normal_object(object, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
impl Vim {
fn object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
match self.mode {
Mode::Normal => self.normal_object(object, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx),
Mode::Insert | Mode::Replace => {
// Shouldn't execute a text object in insert mode. Ignoring
}
}
}
}
impl Object {
pub fn is_multiline(self) -> bool {

View File

@ -3,38 +3,33 @@ use crate::{
state::Mode,
Vim,
};
use editor::{display_map::ToDisplayPoint, Bias, ToPoint};
use gpui::{actions, ViewContext, WindowContext};
use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint};
use gpui::{actions, ViewContext};
use language::{AutoindentMode, Point};
use std::ops::Range;
use std::sync::Arc;
use workspace::Workspace;
actions!(vim, [ToggleReplace, UndoReplace]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext<Workspace>| {
Vim::update(cx, |vim, cx| {
vim.update_state(|state| state.replacements = vec![]);
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &ToggleReplace, cx| {
vim.replacements = vec![];
vim.start_recording(cx);
vim.switch_mode(Mode::Replace, false, cx);
});
});
workspace.register_action(|_, _: &UndoReplace, cx: &mut ViewContext<Workspace>| {
Vim::update(cx, |vim, cx| {
if vim.state().mode != Mode::Replace {
Vim::action(editor, cx, |vim, _: &UndoReplace, cx| {
if vim.mode != Mode::Replace {
return;
}
let count = vim.take_count(cx);
undo_replace(vim, count, cx)
});
vim.undo_replace(count, cx)
});
}
pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
pub(crate) fn multi_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(cx);
@ -58,11 +53,7 @@ pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
.buffer_snapshot
.text_for_range(replace_range.clone())
.collect();
vim.update_state(|state| {
state
.replacements
.push((replace_range.clone(), current_text))
});
vim.replacements.push((replace_range.clone(), current_text));
(replace_range, text.clone())
})
.collect::<Vec<_>>();
@ -83,11 +74,10 @@ pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
editor.set_clip_at_line_ends(true, cx);
});
});
});
}
fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
fn undo_replace(&mut self, maybe_times: Option<usize>, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(cx);
@ -110,14 +100,12 @@ fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContex
let mut undo = None;
let edit_range = start..end;
for (i, (range, inverse)) in vim.state().replacements.iter().rev().enumerate() {
for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
&& range.end.to_point(&map.buffer_snapshot) >= edit_range.end
{
undo = Some(inverse.clone());
vim.update_state(|state| {
state.replacements.remove(state.replacements.len() - i - 1);
});
vim.replacements.remove(vim.replacements.len() - i - 1);
break;
}
}
@ -136,6 +124,7 @@ fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContex
});
});
}
}
#[cfg(test)]
mod test {

View File

@ -1,14 +1,19 @@
use std::borrow::BorrowMut;
use std::{fmt::Display, ops::Range, sync::Arc};
use crate::command::command_interceptor;
use crate::normal::repeat::Replayer;
use crate::surrounds::SurroundsType;
use crate::{motion::Motion, object::Object};
use crate::{UseSystemClipboard, Vim, VimSettings};
use collections::HashMap;
use editor::{Anchor, ClipboardSelection};
use gpui::{Action, ClipboardEntry, ClipboardItem, KeyContext};
use language::{CursorShape, Selection, TransactionId};
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
use editor::{Anchor, ClipboardSelection, Editor};
use gpui::{Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global};
use language::Point;
use serde::{Deserialize, Serialize};
use ui::SharedString;
use settings::{Settings, SettingsStore};
use ui::{SharedString, ViewContext};
use workspace::searchable::Direction;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
@ -75,32 +80,6 @@ pub enum Operator {
ToggleComments,
}
#[derive(Default, Clone)]
pub struct EditorState {
pub mode: Mode,
pub last_mode: Mode,
/// pre_count is the number before an operator is specified (3 in 3d2d)
pub pre_count: Option<usize>,
/// post_count is the number after an operator is specified (2 in 3d2d)
pub post_count: Option<usize>,
pub operator_stack: Vec<Operator>,
pub replacements: Vec<(Range<editor::Anchor>, String)>,
pub marks: HashMap<String, Vec<Anchor>>,
pub stored_visual_mode: Option<(Mode, Vec<bool>)>,
pub change_list: Vec<Vec<Anchor>>,
pub change_list_position: Option<usize>,
pub current_tx: Option<TransactionId>,
pub current_anchor: Option<Selection<Anchor>>,
pub undo_modes: HashMap<TransactionId, Mode>,
pub selected_register: Option<char>,
pub search: SearchState,
}
#[derive(Default, Clone, Debug)]
pub enum RecordedSelection {
#[default]
@ -161,7 +140,7 @@ impl From<String> for Register {
}
#[derive(Default, Clone)]
pub struct WorkspaceState {
pub struct VimGlobals {
pub last_find: Option<Motion>,
pub dot_recording: bool,
@ -182,6 +161,232 @@ pub struct WorkspaceState {
pub registers: HashMap<char, Register>,
pub recordings: HashMap<char, Vec<ReplayableAction>>,
}
impl Global for VimGlobals {}
impl VimGlobals {
pub(crate) fn register(cx: &mut AppContext) {
cx.set_global(VimGlobals::default());
cx.observe_keystrokes(|event, cx| {
let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else {
return;
};
Vim::globals(cx).observe_action(action.boxed_clone())
})
.detach();
cx.observe_global::<SettingsStore>(move |cx| {
if Vim::enabled(cx) {
CommandPaletteFilter::update_global(cx, |filter, _| {
filter.show_namespace(Vim::NAMESPACE);
});
CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
interceptor.set(Box::new(command_interceptor));
});
} else {
*Vim::globals(cx) = VimGlobals::default();
CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
interceptor.clear();
});
CommandPaletteFilter::update_global(cx, |filter, _| {
filter.hide_namespace(Vim::NAMESPACE);
});
}
})
.detach();
}
pub(crate) fn write_registers(
&mut self,
content: Register,
register: Option<char>,
is_yank: bool,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
if let Some(register) = register {
let lower = register.to_lowercase().next().unwrap_or(register);
if lower != register {
let current = self.registers.entry(lower).or_default();
current.text = (current.text.to_string() + &content.text).into();
// not clear how to support appending to registers with multiple cursors
current.clipboard_selections.take();
let yanked = current.clone();
self.registers.insert('"', yanked);
} else {
self.registers.insert('"', content.clone());
match lower {
'_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
'+' => {
cx.write_to_clipboard(content.into());
}
'*' => {
#[cfg(target_os = "linux")]
cx.write_to_primary(content.into());
#[cfg(not(target_os = "linux"))]
cx.write_to_clipboard(content.into());
}
'"' => {
self.registers.insert('0', content.clone());
self.registers.insert('"', content);
}
_ => {
self.registers.insert(lower, content);
}
}
}
} else {
let setting = VimSettings::get_global(cx).use_system_clipboard;
if setting == UseSystemClipboard::Always
|| setting == UseSystemClipboard::OnYank && is_yank
{
self.last_yank.replace(content.text.clone());
cx.write_to_clipboard(content.clone().into());
} else {
self.last_yank = cx
.read_from_clipboard()
.and_then(|item| item.text().map(|string| string.into()));
}
self.registers.insert('"', content.clone());
if is_yank {
self.registers.insert('0', content);
} else {
let contains_newline = content.text.contains('\n');
if !contains_newline {
self.registers.insert('-', content.clone());
}
if linewise || contains_newline {
let mut content = content;
for i in '1'..'8' {
if let Some(moved) = self.registers.insert(i, content) {
content = moved;
} else {
break;
}
}
}
}
}
}
pub(crate) fn read_register(
&mut self,
register: Option<char>,
editor: Option<&mut Editor>,
cx: &ViewContext<Editor>,
) -> Option<Register> {
let Some(register) = register.filter(|reg| *reg != '"') else {
let setting = VimSettings::get_global(cx).use_system_clipboard;
return match setting {
UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => {
cx.read_from_clipboard().map(|item| item.into())
}
_ => self.registers.get(&'"').cloned(),
};
};
let lower = register.to_lowercase().next().unwrap_or(register);
match lower {
'_' | ':' | '.' | '#' | '=' => None,
'+' => cx.read_from_clipboard().map(|item| item.into()),
'*' => {
#[cfg(target_os = "linux")]
{
cx.read_from_primary().map(|item| item.into())
}
#[cfg(not(target_os = "linux"))]
{
cx.read_from_clipboard().map(|item| item.into())
}
}
'%' => editor.and_then(|editor| {
let selection = editor.selections.newest::<Point>(cx);
if let Some((_, buffer, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(selection.head(), cx)
{
buffer
.read(cx)
.file()
.map(|file| file.path().to_string_lossy().to_string().into())
} else {
None
}
}),
_ => self.registers.get(&lower).cloned(),
}
}
fn system_clipboard_is_newer(&self, cx: &ViewContext<Editor>) -> bool {
cx.read_from_clipboard().is_some_and(|item| {
if let Some(last_state) = &self.last_yank {
Some(last_state.as_ref()) != item.text().as_deref()
} else {
true
}
})
}
pub fn observe_action(&mut self, action: Box<dyn Action>) {
if self.dot_recording {
self.recorded_actions
.push(ReplayableAction::Action(action.boxed_clone()));
if self.stop_recording_after_next_action {
self.dot_recording = false;
self.stop_recording_after_next_action = false;
}
}
if self.replayer.is_none() {
if let Some(recording_register) = self.recording_register {
self.recordings
.entry(recording_register)
.or_default()
.push(ReplayableAction::Action(action));
}
}
}
pub fn observe_insertion(&mut self, text: &Arc<str>, range_to_replace: Option<Range<isize>>) {
if self.ignore_current_insertion {
self.ignore_current_insertion = false;
return;
}
if self.dot_recording {
self.recorded_actions.push(ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace.clone(),
});
if self.stop_recording_after_next_action {
self.dot_recording = false;
self.stop_recording_after_next_action = false;
}
}
if let Some(recording_register) = self.recording_register {
self.recordings.entry(recording_register).or_default().push(
ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace,
},
);
}
}
}
impl Vim {
pub fn globals(cx: &mut AppContext) -> &mut VimGlobals {
cx.global_mut::<VimGlobals>()
}
pub fn update_globals<C, R>(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R
where
C: BorrowMut<AppContext>,
{
cx.update_global(f)
}
}
#[derive(Debug)]
pub enum ReplayableAction {
@ -218,93 +423,6 @@ pub struct SearchState {
pub prior_mode: Mode,
}
impl EditorState {
pub fn cursor_shape(&self) -> CursorShape {
match self.mode {
Mode::Normal => {
if self.operator_stack.is_empty() {
CursorShape::Block
} else {
CursorShape::Underscore
}
}
Mode::Replace => CursorShape::Underscore,
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block,
Mode::Insert => CursorShape::Bar,
}
}
pub fn editor_input_enabled(&self) -> bool {
match self.mode {
Mode::Insert => {
if let Some(operator) = self.operator_stack.last() {
!operator.is_waiting(self.mode)
} else {
true
}
}
Mode::Normal | Mode::Replace | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
false
}
}
}
pub fn should_autoindent(&self) -> bool {
!(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock)
}
pub fn clip_at_line_ends(&self) -> bool {
match self.mode {
Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::Replace => {
false
}
Mode::Normal => true,
}
}
pub fn active_operator(&self) -> Option<Operator> {
self.operator_stack.last().cloned()
}
pub fn keymap_context_layer(&self) -> KeyContext {
let mut context = KeyContext::new_with_defaults();
let mut mode = match self.mode {
Mode::Normal => "normal",
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual",
Mode::Insert => "insert",
Mode::Replace => "replace",
}
.to_string();
let mut operator_id = "none";
let active_operator = self.active_operator();
if active_operator.is_none() && self.pre_count.is_some()
|| active_operator.is_some() && self.post_count.is_some()
{
context.add("VimCount");
}
if let Some(active_operator) = active_operator {
if active_operator.is_waiting(self.mode) {
mode = "waiting".to_string();
} else {
mode = "operator".to_string();
operator_id = active_operator.id();
}
}
if mode != "waiting" && mode != "insert" && mode != "replace" {
context.add("VimControl");
}
context.set("vim_mode", mode);
context.set("vim_operator", operator_id);
context
}
}
impl Operator {
pub fn id(&self) -> &'static str {
match self {

View File

@ -5,10 +5,10 @@ use crate::{
Vim,
};
use editor::{movement, scroll::Autoscroll, Bias};
use gpui::WindowContext;
use language::BracketPair;
use serde::Deserialize;
use std::sync::Arc;
use ui::ViewContext;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType {
@ -27,12 +27,17 @@ impl<'de> Deserialize<'de> for SurroundsType {
}
}
pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
let count = vim.take_count(cx);
let mode = vim.state().mode;
vim.update_active_editor(cx, |_, editor, cx| {
impl Vim {
pub fn add_surrounds(
&mut self,
text: Arc<str>,
target: SurroundsType,
cx: &mut ViewContext<Self>,
) {
self.stop_recording(cx);
let count = self.take_count(cx);
let mode = self.mode;
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -128,13 +133,11 @@ pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowConte
});
});
});
vim.switch_mode(Mode::Normal, false, cx);
});
self.switch_mode(Mode::Normal, false, cx);
}
pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
pub fn delete_surrounds(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
self.stop_recording(cx);
// only legitimate surrounds can be removed
let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
@ -147,7 +150,7 @@ pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
};
let surround = pair.end != *text;
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -224,14 +227,12 @@ pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
editor.set_clip_at_line_ends(true, cx);
});
});
});
}
pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
pub fn change_surrounds(&mut self, text: Arc<str>, target: Object, cx: &mut ViewContext<Self>) {
if let Some(will_replace_pair) = object_to_bracket_pair(target) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -332,7 +333,6 @@ pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext)
});
});
});
});
}
}
@ -344,13 +344,13 @@ pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext)
/// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
/// If no valid pair of brackets is found for any cursor, the method returns `false`.
pub fn check_and_move_to_valid_bracket_pair(
vim: &mut Vim,
&mut self,
object: Object,
cx: &mut WindowContext,
cx: &mut ViewContext<Self>,
) -> bool {
let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) {
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
@ -368,7 +368,9 @@ pub fn check_and_move_to_valid_bracket_pair(
{
valid = true;
let mut chars_and_offset = display_map
.buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
.buffer_chars_at(
range.start.to_offset(&display_map, Bias::Left),
)
.peekable();
while let Some((ch, offset)) = chars_and_offset.next() {
if ch.to_string() == pair.start {
@ -392,6 +394,7 @@ pub fn check_and_move_to_valid_bracket_pair(
}
return valid;
}
}
fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
pairs.iter().find(|pair| pair.start == ch || pair.end == ch)

View File

@ -17,7 +17,7 @@ use indoc::indoc;
use search::BufferSearchBar;
use workspace::WorkspaceSettings;
use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator};
use crate::{insert::NormalBefore, motion, state::Mode};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@ -51,7 +51,7 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("hjklˇ");
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
cx.set_state("«hjklˇ»", Mode::Normal);
cx.cx.set_state("«hjklˇ»");
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.blur());
cx.assert_editor_state("«hjklˇ»");
@ -279,59 +279,6 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
}
#[gpui::test]
async fn test_status_indicator(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let mode_indicator = cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
assert!(mode_indicator.is_some());
mode_indicator.unwrap()
});
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Some(Mode::Normal)
);
// shows the correct mode
cx.simulate_keystrokes("i");
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Some(Mode::Insert)
);
cx.simulate_keystrokes("escape shift-r");
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Some(Mode::Replace)
);
// shows even in search
cx.simulate_keystrokes("escape v /");
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Some(Mode::Visual)
);
// hides if vim mode is disabled
cx.disable_vim();
cx.run_until_parked();
cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
assert!(mode_indicator.read(cx).mode.is_none());
});
cx.enable_vim();
cx.run_until_parked();
cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
assert!(mode_indicator.read(cx).mode.is_some());
});
}
#[gpui::test]
async fn test_word_characters(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await;

View File

@ -10,7 +10,7 @@ use language::language_settings::{AllLanguageSettings, SoftWrap};
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, VimTestContext};
use crate::{state::Mode, Vim};
use crate::state::{Mode, VimGlobals};
pub struct NeovimBackedTestContext {
cx: VimTestContext,
@ -263,8 +263,7 @@ impl NeovimBackedTestContext {
state: self.shared_state().await,
neovim: self.neovim.read_register(register).await,
editor: self.update(|cx| {
Vim::read(cx)
.workspace_state
cx.global::<VimGlobals>()
.registers
.get(&register)
.cloned()

View File

@ -1,7 +1,7 @@
use std::ops::{Deref, DerefMut};
use editor::test::editor_lsp_test_context::EditorLspTestContext;
use gpui::{Context, SemanticVersion, View, VisualContext};
use gpui::{Context, SemanticVersion, UpdateGlobal, View, VisualContext};
use search::{project_search::ProjectSearchBar, BufferSearchBar};
use crate::{state::Operator, *};
@ -12,7 +12,7 @@ pub struct VimTestContext {
impl VimTestContext {
pub fn init(cx: &mut gpui::TestAppContext) {
if cx.has_global::<Vim>() {
if cx.has_global::<VimGlobals>() {
return;
}
cx.update(|cx| {
@ -119,23 +119,31 @@ impl VimTestContext {
}
pub fn mode(&mut self) -> Mode {
self.cx.read(|cx| cx.global::<Vim>().state().mode)
self.update_editor(|editor, cx| editor.addon::<VimAddon>().unwrap().view.read(cx).mode)
}
pub fn active_operator(&mut self) -> Option<Operator> {
self.cx
.read(|cx| cx.global::<Vim>().state().operator_stack.last().cloned())
self.update_editor(|editor, cx| {
editor
.addon::<VimAddon>()
.unwrap()
.view
.read(cx)
.operator_stack
.last()
.cloned()
})
}
pub fn set_state(&mut self, text: &str, mode: Mode) {
let window = self.window;
self.cx.set_state(text);
self.update_window(window, |_, cx| {
Vim::update(cx, |vim, cx| {
let vim = self.update_editor(|editor, _cx| editor.addon::<VimAddon>().cloned().unwrap());
self.update(|cx| {
vim.view.update(cx, |vim, cx| {
vim.switch_mode(mode, true, cx);
})
})
.unwrap();
});
});
self.cx.cx.cx.run_until_parked();
}

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,15 @@ use editor::{
scroll::Autoscroll,
Bias, DisplayPoint, Editor, ToOffset,
};
use gpui::{actions, ViewContext, WindowContext};
use gpui::{actions, ViewContext};
use language::{Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow;
use search::BufferSearchBar;
use util::ResultExt;
use workspace::{searchable::Direction, Workspace};
use workspace::searchable::Direction;
use crate::{
motion::{start_of_line, Motion},
normal::yank::{copy_selections_content, yank_selections_content},
normal::{mark::create_visual_marks, substitute::substitute},
object::Object,
state::{Mode, Operator},
Vim,
@ -41,59 +39,41 @@ actions!(
]
);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::Visual, cx)
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| {
vim.toggle_mode(Mode::Visual, cx)
});
workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::VisualLine, cx)
Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| {
vim.toggle_mode(Mode::VisualLine, cx)
});
workspace.register_action(
|_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::VisualBlock, cx)
},
);
workspace.register_action(other_end);
workspace.register_action(|_, _: &VisualDelete, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| {
vim.toggle_mode(Mode::VisualBlock, cx)
});
Vim::action(editor, cx, Vim::other_end);
Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
vim.record_current_action(cx);
delete(vim, false, cx);
vim.visual_delete(false, cx);
});
});
workspace.register_action(|_, _: &VisualDeleteLine, cx| {
Vim::update(cx, |vim, cx| {
Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| {
vim.record_current_action(cx);
delete(vim, true, cx);
vim.visual_delete(true, cx);
});
Vim::action(editor, cx, |vim, _: &VisualYank, cx| vim.visual_yank(cx));
Vim::action(editor, cx, Vim::select_next);
Vim::action(editor, cx, Vim::select_previous);
Vim::action(editor, cx, |vim, _: &SelectNextMatch, cx| {
vim.select_match(Direction::Next, cx);
});
workspace.register_action(|_, _: &VisualYank, cx| {
Vim::update(cx, |vim, cx| {
yank(vim, cx);
});
Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| {
vim.select_match(Direction::Prev, cx);
});
workspace.register_action(select_next);
workspace.register_action(select_previous);
workspace.register_action(|workspace, _: &SelectNextMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Next, cx);
});
});
workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Prev, cx);
});
});
workspace.register_action(|_, _: &RestoreVisualSelection, cx| {
Vim::update(cx, |vim, cx| {
let Some((stored_mode, reversed)) =
vim.update_state(|state| state.stored_visual_mode.take())
else {
Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| {
let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
return;
};
let Some((start, end)) = vim.state().marks.get("<").zip(vim.state().marks.get(">"))
else {
let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
return;
};
let ranges = start
@ -103,18 +83,17 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
.map(|((start, end), reversed)| (*start, *end, reversed))
.collect::<Vec<_>>();
if vim.state().mode.is_visual() {
create_visual_marks(vim, vim.state().mode, cx);
if vim.mode.is_visual() {
vim.create_visual_marks(vim.mode, cx);
}
vim.update_active_editor(cx, |_, editor, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = s.display_map();
let ranges = ranges
.into_iter()
.map(|(start, end, reversed)| {
let new_end =
movement::saturating_right(&map, end.to_display_point(&map));
let new_end = movement::saturating_right(&map, end.to_display_point(&map));
Selection {
id: s.new_selection_id(),
start: start.to_offset(&map.buffer_snapshot),
@ -129,14 +108,18 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
});
vim.switch_mode(stored_mode, true, cx)
});
});
}
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
pub fn visual_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
if vim.state().mode == Mode::VisualBlock
if vim.mode == Mode::VisualBlock
&& !matches!(
motion,
Motion::EndOfLine {
@ -145,7 +128,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
)
{
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
vim.visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
motion.move_point(map, point, goal, times, &text_layout_details)
})
} else {
@ -183,7 +166,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
// ensure the current character is included in the selection.
if !selection.reversed {
let next_point = if vim.state().mode == Mode::VisualBlock {
let next_point = if vim.mode == Mode::VisualBlock {
movement::saturating_right(map, selection.end)
} else {
movement::right(map, selection.end)
@ -206,10 +189,10 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
});
}
});
});
}
pub fn visual_block_motion(
&mut self,
preserve_goal: bool,
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
@ -316,17 +299,16 @@ pub fn visual_block_motion(
})
}
pub fn visual_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if let Some(Operator::Object { around }) = vim.active_operator() {
vim.pop_operator(cx);
let current_mode = vim.state().mode;
pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext<Vim>) {
if let Some(Operator::Object { around }) = self.active_operator() {
self.pop_operator(cx);
let current_mode = self.mode;
let target_mode = object.target_visual_mode(current_mode);
if target_mode != current_mode {
vim.switch_mode(target_mode, true, cx);
self.switch_mode(target_mode, true, cx);
}
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut mut_selection = selection.clone();
@ -384,34 +366,29 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
});
});
}
});
}
fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
if vim.state().mode == mode {
vim.switch_mode(Mode::Normal, false, cx);
fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
if self.mode == mode {
self.switch_mode(Mode::Normal, false, cx);
} else {
vim.switch_mode(mode, false, cx);
self.switch_mode(mode, false, cx);
}
})
}
pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
})
})
})
});
}
pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
vim.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext<Self>) {
self.store_visual_marks(cx);
self.update_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode;
@ -424,9 +401,9 @@ pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
position = movement::left(map, position);
}
original_columns.insert(selection.id, position.to_point(map).column);
if vim.state().mode == Mode::VisualBlock {
if vim.mode == Mode::VisualBlock {
*selection.end.column_mut() = map.line_len(selection.end.row())
} else if vim.state().mode != Mode::VisualLine {
} else if vim.mode != Mode::VisualLine {
selection.start = DisplayPoint::new(selection.start.row(), 0);
if selection.end.row() == map.max_point().row() {
selection.end = map.max_point()
@ -439,7 +416,7 @@ pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
selection.goal = SelectionGoal::None;
});
});
copy_selections_content(vim, editor, line_mode, cx);
vim.copy_selections_content(editor, line_mode, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
@ -454,20 +431,20 @@ pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
if vim.state().mode == Mode::VisualBlock {
if vim.mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
})
});
vim.switch_mode(Mode::Normal, true, cx);
self.switch_mode(Mode::Normal, true, cx);
}
pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
vim.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
pub fn visual_yank(&mut self, cx: &mut ViewContext<Self>) {
self.store_visual_marks(cx);
self.update_editor(cx, |vim, editor, cx| {
let line_mode = editor.selections.line_mode;
yank_selections_content(vim, editor, line_mode, cx);
vim.yank_selections_content(editor, line_mode, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if line_mode {
@ -475,18 +452,17 @@ pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
};
selection.collapse_to(selection.start, SelectionGoal::None)
});
if vim.state().mode == Mode::VisualBlock {
if vim.mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
});
vim.switch_mode(Mode::Normal, true, cx);
self.switch_mode(Mode::Normal, true, cx);
}
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
@ -522,16 +498,14 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
});
});
vim.switch_mode(Mode::Normal, false, cx);
});
self.switch_mode(Mode::Normal, false, cx);
}
pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let count = self
.take_count(cx)
.unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
for _ in 0..count {
if editor
@ -542,16 +516,14 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Works
break;
}
}
})
});
}
pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
let count = self
.take_count(cx)
.unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(cx, |_, editor, cx| {
for _ in 0..count {
if editor
.select_previous(&Default::default(), cx)
@ -561,28 +533,30 @@ pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewConte
break;
}
}
})
});
}
pub fn select_match(
workspace: &mut Workspace,
vim: &mut Vim,
direction: Direction,
cx: &mut WindowContext,
) {
let count = vim.take_count(cx).unwrap_or(1);
let pane = workspace.active_pane().clone();
let vim_is_normal = vim.state().mode == Mode::Normal;
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx).unwrap_or(1);
let Some(workspace) = self
.editor
.upgrade()
.and_then(|editor| editor.read(cx).workspace())
else {
return;
};
let pane = workspace.read(cx).active_pane().clone();
let vim_is_normal = self.mode == Mode::Normal;
let mut start_selection = 0usize;
let mut end_selection = 0usize;
vim.update_active_editor(cx, |_, editor, _| {
self.update_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false);
});
if vim_is_normal {
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
{
search_bar.update(cx, |search_bar, cx| {
if !search_bar.has_active_match() || !search_bar.show(cx) {
return;
@ -594,7 +568,7 @@ pub fn select_match(
}
});
}
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
start_selection = latest.start;
end_selection = latest.end;
@ -611,11 +585,11 @@ pub fn select_match(
}
});
if !match_exists {
vim.clear_operator(cx);
vim.stop_replaying(cx);
self.clear_operator(cx);
self.stop_replaying(cx);
return;
}
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
if vim_is_normal {
start_selection = latest.start;
@ -633,17 +607,17 @@ pub fn select_match(
editor.set_collapse_matches(true);
});
match vim.maybe_pop_operator() {
Some(Operator::Change) => substitute(vim, None, false, cx),
match self.maybe_pop_operator() {
Some(Operator::Change) => self.substitute(None, false, cx),
Some(Operator::Delete) => {
vim.stop_recording();
delete(vim, false, cx)
self.stop_recording(cx);
self.visual_delete(false, cx)
}
Some(Operator::Yank) => yank(vim, cx),
Some(Operator::Yank) => self.visual_yank(cx),
_ => {} // Ignoring other operators
}
}
}
#[cfg(test)]
mod test {
use indoc::indoc;