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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ use collections::HashMap;
use gpui::AppContext; use gpui::AppContext;
use settings::Settings; use settings::Settings;
use std::sync::LazyLock; use std::sync::LazyLock;
use ui::WindowContext; use ui::ViewContext;
use crate::{Vim, VimSettings}; 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()) .unwrap_or_else(|| b.to_string().into())
} }
pub fn insert_digraph(first_char: char, second_char: char, cx: &mut WindowContext) { impl Vim {
let text = lookup_digraph(first_char, second_char, &cx); 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)); self.pop_operator(cx);
if Vim::read(cx).state().editor_input_enabled() { if self.editor_input_enabled() {
Vim::update(cx, |vim, cx| { self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx));
vim.update_active_editor(cx, |_, editor, cx| editor.insert(&text, cx)); } else {
}); self.input_ignored(text, cx);
} else { }
Vim::active_editor_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::{ use crate::{state::Mode, Vim};
normal::{mark::create_mark, repeat}, use editor::{scroll::Autoscroll, Bias, Editor};
state::Mode,
Vim,
};
use editor::{scroll::Autoscroll, Bias};
use gpui::{actions, Action, ViewContext}; use gpui::{actions, Action, ViewContext};
use language::SelectionGoal; use language::SelectionGoal;
use workspace::Workspace;
actions!(vim, [NormalBefore]); actions!(vim, [NormalBefore]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(normal_before); Vim::action(editor, cx, Vim::normal_before);
} }
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) { impl Vim {
let should_repeat = Vim::update(cx, |vim, cx| { fn normal_before(&mut self, action: &NormalBefore, cx: &mut ViewContext<Self>) {
if vim.state().active_operator().is_some() { if self.active_operator().is_some() {
vim.update_state(|state| state.operator_stack.clear()); self.operator_stack.clear();
vim.sync_vim_settings(cx); self.sync_vim_settings(cx);
return false; return;
} }
let count = vim.take_count(cx).unwrap_or(1); let count = self.take_count(cx).unwrap_or(1);
vim.stop_recording_immediately(action.boxed_clone()); self.stop_recording_immediately(action.boxed_clone(), cx);
if count <= 1 || vim.workspace_state.dot_replaying { if count <= 1 || Vim::globals(cx).dot_replaying {
create_mark(vim, "^".into(), false, cx); self.create_mark("^".into(), false, cx);
vim.update_active_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.dismiss_menus_and_popups(false, cx); editor.dismiss_menus_and_popups(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| { 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); self.switch_mode(Mode::Normal, false, cx);
false return;
} else {
true
} }
});
if should_repeat { self.repeat(true, cx)
repeat::repeat(cx, true)
} }
} }

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 itertools::Itertools;
use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; 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. /// The ModeIndicator displays the current mode in the status bar.
pub struct ModeIndicator { pub struct ModeIndicator {
pub(crate) mode: Option<Mode>, vim: Option<WeakView<Vim>>,
pub(crate) operators: String,
pending_keys: Option<String>, pending_keys: Option<String>,
_subscriptions: Vec<Subscription>, vim_subscription: Option<Subscription>,
} }
impl ModeIndicator { impl ModeIndicator {
/// Construct a new mode indicator in this window. /// Construct a new mode indicator in this window.
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
let _subscriptions = vec![ cx.observe_pending_input(|this, cx| {
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)), this.update_pending_keys(cx);
cx.observe_pending_input(|this, cx| { cx.notify();
this.update_pending_keys(cx); })
cx.notify(); .detach();
}),
];
let mut this = Self { let handle = cx.view().clone();
mode: None, let window = cx.window_handle();
operators: "".to_string(), 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();
Self {
vim: None,
pending_keys: None, pending_keys: None,
_subscriptions, vim_subscription: None,
};
this.update_mode(cx);
this
}
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;
} }
} }
fn update_pending_keys(&mut self, cx: &mut ViewContext<Self>) { 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| {
self.pending_keys = cx.pending_input_keystrokes().map(|keystrokes| { keystrokes
keystrokes .iter()
.iter() .map(|keystroke| format!("{}", keystroke))
.map(|keystroke| format!("{}", keystroke)) .join(" ")
.join(" ") });
});
} else {
self.pending_keys = None;
}
} }
fn vim<'a>(&self, cx: &'a mut ViewContext<Self>) -> Option<&'a Vim> { fn vim(&self) -> Option<View<Vim>> {
// In some tests Vim isn't enabled, so we use try_global. self.vim.as_ref().and_then(|vim| vim.upgrade())
cx.try_global::<Vim>().filter(|vim| vim.enabled)
} }
fn current_operators_description(&self, vim: &Vim) -> String { fn current_operators_description(&self, vim: View<Vim>, cx: &mut ViewContext<Self>) -> String {
vim.workspace_state let recording = Vim::globals(cx)
.recording_register .recording_register
.map(|reg| format!("recording @{reg} ")) .map(|reg| format!("recording @{reg} "))
.into_iter() .into_iter();
.chain(vim.state().pre_count.map(|count| format!("{}", count)))
.chain(vim.state().selected_register.map(|reg| format!("\"{reg}"))) let vim = vim.read(cx);
.chain( recording
vim.state() .chain(vim.pre_count.map(|count| format!("{}", count)))
.operator_stack .chain(vim.selected_register.map(|reg| format!("\"{reg}")))
.iter() .chain(vim.operator_stack.iter().map(|item| item.id().to_string()))
.map(|item| item.id().to_string()), .chain(vim.post_count.map(|count| format!("{}", count)))
)
.chain(vim.state().post_count.map(|count| format!("{}", count)))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("") .join("")
} }
} }
impl Render for ModeIndicator { impl Render for ModeIndicator {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(mode) = self.mode.as_ref() else { let vim = self.vim();
let Some(vim) = vim else {
return div().into_any(); return div().into_any();
}; };
let pending = self.pending_keys.as_ref().unwrap_or(&self.operators); let current_operators_description = self.current_operators_description(vim.clone(), cx);
let pending = self
Label::new(format!("{} -- {} --", pending, mode)) .pending_keys
.as_ref()
.unwrap_or(&current_operators_description);
Label::new(format!("{} -- {} --", pending, vim.read(cx).mode))
.size(LabelSize::Small) .size(LabelSize::Small)
.line_height_style(LineHeightStyle::UiLabel) .line_height_style(LineHeightStyle::UiLabel)
.into_any_element() .into_any_element()
@ -100,6 +102,5 @@ impl StatusItemView for ModeIndicator {
_active_pane_item: Option<&dyn ItemHandle>, _active_pane_item: Option<&dyn ItemHandle>,
_cx: &mut ViewContext<Self>, _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, self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
}, },
scroll::Autoscroll, 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 language::{char_kind, CharKind, Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use serde::Deserialize; use serde::Deserialize;
use std::ops::Range; use std::ops::Range;
use workspace::Workspace;
use crate::{ use crate::{
normal::{mark, normal_motion}, normal::mark,
state::{Mode, Operator}, state::{Mode, Operator},
surrounds::SurroundsType, surrounds::SurroundsType,
visual::visual_motion,
Vim, Vim,
}; };
@ -248,214 +246,227 @@ actions!(
] ]
); );
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx));
workspace Vim::action(editor, cx, |vim, _: &Backspace, cx| {
.register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); vim.motion(Motion::Backspace, cx)
workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| { });
motion( Vim::action(editor, cx, |vim, action: &Down, cx| {
vim.motion(
Motion::Down { Motion::Down {
display_lines: action.display_lines, display_lines: action.display_lines,
}, },
cx, cx,
) )
}); });
workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| { Vim::action(editor, cx, |vim, action: &Up, cx| {
motion( vim.motion(
Motion::Up { Motion::Up {
display_lines: action.display_lines, display_lines: action.display_lines,
}, },
cx, cx,
) )
}); });
workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); Vim::action(editor, cx, |vim, _: &Right, cx| {
workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx)); vim.motion(Motion::Right, cx)
workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { });
motion( Vim::action(editor, cx, |vim, _: &Space, cx| {
vim.motion(Motion::Space, cx)
});
Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| {
vim.motion(
Motion::FirstNonWhitespace { Motion::FirstNonWhitespace {
display_lines: action.display_lines, display_lines: action.display_lines,
}, },
cx, cx,
) )
}); });
workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { Vim::action(editor, cx, |vim, action: &StartOfLine, cx| {
motion( vim.motion(
Motion::StartOfLine { Motion::StartOfLine {
display_lines: action.display_lines, display_lines: action.display_lines,
}, },
cx, cx,
) )
}); });
workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { Vim::action(editor, cx, |vim, action: &EndOfLine, cx| {
motion( vim.motion(
Motion::EndOfLine { Motion::EndOfLine {
display_lines: action.display_lines, display_lines: action.display_lines,
}, },
cx, cx,
) )
}); });
workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| { Vim::action(editor, cx, |vim, _: &CurrentLine, cx| {
motion(Motion::CurrentLine, cx) vim.motion(Motion::CurrentLine, cx)
}); });
workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| {
motion(Motion::StartOfParagraph, cx) vim.motion(Motion::StartOfParagraph, cx)
}); });
workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| { Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
motion(Motion::EndOfParagraph, cx) vim.motion(Motion::EndOfParagraph, cx)
}); });
workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
motion(Motion::StartOfDocument, cx) vim.motion(Motion::StartOfDocument, cx)
}); });
workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| { Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| {
motion(Motion::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( Vim::action(
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| { editor,
motion(Motion::NextWordStart { ignore_punctuation }, cx) cx,
|vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| {
vim.motion(Motion::NextWordStart { ignore_punctuation }, cx)
}, },
); );
workspace.register_action( Vim::action(
|_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| { editor,
motion(Motion::NextWordEnd { ignore_punctuation }, cx) cx,
|vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| {
vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx)
}, },
); );
workspace.register_action( Vim::action(
|_: &mut Workspace, editor,
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) }, |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| {
); vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx)
workspace.register_action(
|_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
}, },
); );
workspace.register_action( Vim::action(
|_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| { editor,
motion(Motion::NextSubwordStart { ignore_punctuation }, cx) cx,
|vim, &PreviousWordEnd { ignore_punctuation }, cx| {
vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
}, },
); );
workspace.register_action( Vim::action(
|_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| { editor,
motion(Motion::NextSubwordEnd { ignore_punctuation }, cx) cx,
|vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| {
vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
}, },
); );
workspace.register_action( Vim::action(
|_: &mut Workspace, editor,
&PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx,
cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) }, |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| {
); vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
workspace.register_action(
|_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
}, },
); );
workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| { Vim::action(
motion(Motion::NextLineStart, cx) 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: _| { Vim::action(editor, cx, |vim, &PreviousLineStart, cx| {
motion(Motion::PreviousLineStart, cx) vim.motion(Motion::PreviousLineStart, cx)
}); });
workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| { Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| {
motion(Motion::StartOfLineDownward, cx) vim.motion(Motion::StartOfLineDownward, cx)
}); });
workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| {
motion(Motion::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: _| { Vim::action(editor, cx, |vim, _: &RepeatFind, cx| {
if let Some(last_find) = Vim::read(cx) if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
.workspace_state vim.motion(Motion::RepeatFind { last_find }, cx);
.last_find
.clone()
.map(Box::new)
{
motion(Motion::RepeatFind { last_find }, cx);
} }
}); });
workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| { Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| {
if let Some(last_find) = Vim::read(cx) if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
.workspace_state vim.motion(Motion::RepeatFindReversed { last_find }, cx);
.last_find
.clone()
.map(Box::new)
{
motion(Motion::RepeatFindReversed { last_find }, cx);
} }
}); });
workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx)); Vim::action(editor, cx, |vim, &WindowTop, cx| {
workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| { vim.motion(Motion::WindowTop, cx)
motion(Motion::WindowMiddle, cx)
}); });
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| { Vim::action(editor, cx, |vim, &WindowMiddle, cx| {
motion(Motion::WindowBottom, 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 {
if let Motion::ZedSearchResult { pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext<Self>) {
prior_selections, .. if let Motion::ZedSearchResult {
} = &m prior_selections, ..
{ } = &m
match Vim::read(cx).state().mode { {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { match self.mode {
if !prior_selections.is_empty() { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
Vim::update(cx, |vim, cx| { if !prior_selections.is_empty() {
vim.update_active_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(prior_selections.iter().cloned()) s.select_ranges(prior_selections.iter().cloned())
}) })
}); });
}); }
}
Mode::Normal | Mode::Replace | Mode::Insert => {
if self.active_operator().is_none() {
return;
}
} }
} }
}
self.motion(m, cx)
}
pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
self.active_operator()
{
self.pop_operator(cx);
}
let count = self.take_count(cx);
let active_operator = self.active_operator();
let mut waiting_operator: Option<Operator> = None;
match self.mode {
Mode::Normal | Mode::Replace | Mode::Insert => { Mode::Normal | Mode::Replace | Mode::Insert => {
if Vim::read(cx).active_operator().is_none() { if active_operator == Some(Operator::AddSurrounds { target: None }) {
return; waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Motion(motion)),
});
} else {
self.normal_motion(motion.clone(), active_operator.clone(), count, cx)
} }
} }
} Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
} self.visual_motion(motion.clone(), count, cx)
motion(m, cx)
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
Vim::read(cx).active_operator()
{
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
let active_operator = Vim::read(cx).active_operator();
let mut waiting_operator: Option<Operator> = None;
match Vim::read(cx).state().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)
} }
} }
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { self.clear_operator(cx);
visual_motion(motion.clone(), count, cx) if let Some(operator) = waiting_operator {
self.push_operator(operator, cx);
self.pre_count = count
} }
} }
Vim::update(cx, |vim, cx| {
vim.clear_operator(cx);
if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
vim.update_state(|state| state.pre_count = count)
}
});
} }
// Motion handling is specified here: // Motion handling is specified here:

View File

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

View File

@ -3,8 +3,6 @@ use editor::{display_map::ToDisplayPoint, scroll::Autoscroll};
use gpui::ViewContext; use gpui::ViewContext;
use language::{Bias, Point, SelectionGoal}; use language::{Bias, Point, SelectionGoal};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use ui::WindowContext;
use workspace::Workspace;
use crate::{ use crate::{
motion::Motion, motion::Motion,
@ -20,120 +18,112 @@ pub enum CaseTarget {
OppositeCase, OppositeCase,
} }
pub fn change_case_motion( impl Vim {
vim: &mut Vim, pub fn change_case_motion(
motion: Motion, &mut self,
times: Option<usize>, motion: Motion,
mode: CaseTarget, times: Option<usize>,
cx: &mut WindowContext, mode: CaseTarget,
) { cx: &mut ViewContext<Self>,
vim.stop_recording(); ) {
vim.update_active_editor(cx, |_, editor, cx| { self.stop_recording(cx);
let text_layout_details = editor.text_layout_details(cx); self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx);
let mut selection_starts: HashMap<_, _> = Default::default(); editor.transact(cx, |editor, cx| {
editor.change_selections(None, cx, |s| { let mut selection_starts: HashMap<_, _> = Default::default();
s.move_with(|map, selection| { editor.change_selections(None, cx, |s| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); s.move_with(|map, selection| {
selection_starts.insert(selection.id, anchor); let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
motion.expand_selection(map, selection, times, false, &text_layout_details); selection_starts.insert(selection.id, anchor);
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
}); });
}); match mode {
match mode { CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx),
CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx),
CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), CaseTarget::OppositeCase => {
CaseTarget::OppositeCase => { editor.convert_to_opposite_case(&Default::default(), cx)
editor.convert_to_opposite_case(&Default::default(), cx) }
} }
} editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = selection_starts.remove(&selection.id).unwrap();
let anchor = selection_starts.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
}
pub fn change_case_object( pub fn change_case_object(
vim: &mut Vim, &mut self,
object: Object, object: Object,
around: bool, around: bool,
mode: CaseTarget, mode: CaseTarget,
cx: &mut WindowContext, cx: &mut ViewContext<Self>,
) { ) {
vim.stop_recording(); self.stop_recording(cx);
vim.update_active_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
let mut original_positions: HashMap<_, _> = Default::default(); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
object.expand_selection(map, selection, around); object.expand_selection(map, selection, around);
original_positions.insert( original_positions.insert(
selection.id, selection.id,
map.display_point_to_anchor(selection.start, Bias::Left), map.display_point_to_anchor(selection.start, Bias::Left),
); );
});
}); });
}); match mode {
match mode { CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx),
CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx),
CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), CaseTarget::OppositeCase => {
CaseTarget::OppositeCase => { editor.convert_to_opposite_case(&Default::default(), cx)
editor.convert_to_opposite_case(&Default::default(), cx) }
} }
} editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = original_positions.remove(&selection.id).unwrap();
let anchor = original_positions.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
}
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) { pub fn change_case(&mut self, _: &ChangeCase, cx: &mut ViewContext<Self>) {
manipulate_text(cx, |c| { self.manipulate_text(cx, |c| {
if c.is_lowercase() { if c.is_lowercase() {
c.to_uppercase().collect::<Vec<char>>() c.to_uppercase().collect::<Vec<char>>()
} else { } else {
c.to_lowercase().collect::<Vec<char>>() c.to_lowercase().collect::<Vec<char>>()
} }
}) })
} }
pub fn convert_to_upper_case( pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
_: &mut Workspace, self.manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
_: &ConvertToUpperCase, }
cx: &mut ViewContext<Workspace>,
) {
manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
}
pub fn convert_to_lower_case( pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
_: &mut Workspace, self.manipulate_text(cx, |c| c.to_lowercase().collect::<Vec<char>>())
_: &ConvertToLowerCase, }
cx: &mut ViewContext<Workspace>,
) {
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 where
F: Fn(char) -> Vec<char> + Copy, F: Fn(char) -> Vec<char> + Copy,
{ {
Vim::update(cx, |vim, cx| { self.record_current_action(cx);
vim.record_current_action(cx); self.store_visual_marks(cx);
vim.store_visual_marks(cx); let count = self.take_count(cx).unwrap_or(1) as u32;
let count = vim.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 ranges = Vec::new();
let mut cursor_positions = Vec::new(); let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) { for selection in editor.selections.all::<Point>(cx) {
match vim.state().mode { match vim.mode {
Mode::VisualLine => { Mode::VisualLine => {
let start = Point::new(selection.start.row, 0); let start = Point::new(selection.start.row, 0);
let end = Point::new( 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)] #[cfg(test)]

View File

@ -1,6 +1,5 @@
use crate::{ use crate::{
motion::{self, Motion}, motion::{self, Motion},
normal::yank::copy_selections_content,
object::Object, object::Object,
state::Mode, state::Mode,
Vim, Vim,
@ -11,98 +10,108 @@ use editor::{
scroll::Autoscroll, scroll::Autoscroll,
Bias, DisplayPoint, Bias, DisplayPoint,
}; };
use gpui::WindowContext;
use language::{char_kind, CharKind, Selection}; 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 {
// Some motions ignore failure when switching to normal mode pub fn change_motion(
let mut motion_succeeded = matches!( &mut self,
motion, motion: Motion,
Motion::Left times: Option<usize>,
| Motion::Right cx: &mut ViewContext<Self>,
| Motion::EndOfLine { .. } ) {
| Motion::Backspace // Some motions ignore failure when switching to normal mode
| Motion::StartOfLine { .. } let mut motion_succeeded = matches!(
); motion,
vim.update_active_editor(cx, |vim, editor, cx| { Motion::Left
let text_layout_details = editor.text_layout_details(cx); | Motion::Right
editor.transact(cx, |editor, cx| { | Motion::EndOfLine { .. }
| Motion::Backspace
| Motion::StartOfLine { .. }
);
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
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
motion_succeeded |= match motion {
Motion::NextWordStart { ignore_punctuation }
| Motion::NextSubwordStart { ignore_punctuation } => {
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
motion == Motion::NextSubwordStart { ignore_punctuation },
)
}
_ => {
let result = motion.expand_selection(
map,
selection,
times,
false,
&text_layout_details,
);
if let Motion::CurrentLine = motion {
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
{
break;
}
start_offset = offset + ch.len_utf8();
}
selection.start = start_offset.to_display_point(map);
}
result
}
}
});
});
vim.copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
if motion_succeeded {
self.switch_mode(Mode::Insert, false, cx)
} else {
self.switch_mode(Mode::Normal, false, cx)
}
}
pub fn change_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
let mut objects_found = false;
self.update_editor(cx, |vim, editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now // We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.transact(cx, |editor, cx| {
s.move_with(|map, selection| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
motion_succeeded |= match motion { s.move_with(|map, selection| {
Motion::NextWordStart { ignore_punctuation } objects_found |= object.expand_selection(map, selection, around);
| Motion::NextSubwordStart { ignore_punctuation } => { });
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
motion == Motion::NextSubwordStart { ignore_punctuation },
)
}
_ => {
let result = motion.expand_selection(
map,
selection,
times,
false,
&text_layout_details,
);
if let Motion::CurrentLine = motion {
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 {
break;
}
start_offset = offset + ch.len_utf8();
}
selection.start = start_offset.to_display_point(map);
}
result
}
}
}); });
if objects_found {
vim.copy_selections_content(editor, false, cx);
editor.insert("", cx);
}
}); });
copy_selections_content(vim, editor, motion.linewise(), cx);
editor.insert("", cx);
}); });
});
if motion_succeeded { if objects_found {
vim.switch_mode(Mode::Insert, false, cx) self.switch_mode(Mode::Insert, false, cx);
} else { } 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) {
let mut objects_found = false;
vim.update_active_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| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
objects_found |= object.expand_selection(map, selection, around);
});
});
if objects_found {
copy_selections_content(vim, editor, false, cx);
editor.insert("", cx);
}
});
});
if objects_found {
vim.switch_mode(Mode::Insert, false, cx);
} else {
vim.switch_mode(Mode::Normal, false, cx);
} }
} }

View File

@ -1,146 +1,154 @@
use crate::{motion::Motion, normal::yank::copy_selections_content, object::Object, Vim}; use crate::{motion::Motion, object::Object, Vim};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use editor::{ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
scroll::Autoscroll, scroll::Autoscroll,
Bias, DisplayPoint, Bias, DisplayPoint,
}; };
use gpui::WindowContext;
use language::{Point, Selection}; use language::{Point, Selection};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use ui::ViewContext;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) { impl Vim {
vim.stop_recording(); pub fn delete_motion(
vim.update_active_editor(cx, |vim, editor, cx| { &mut self,
let text_layout_details = editor.text_layout_details(cx); motion: Motion,
editor.transact(cx, |editor, cx| { times: Option<usize>,
editor.set_clip_at_line_ends(false, cx); cx: &mut ViewContext<Self>,
let mut original_columns: HashMap<_, _> = Default::default(); ) {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { self.stop_recording(cx);
s.move_with(|map, selection| { self.update_editor(cx, |vim, editor, cx| {
let original_head = selection.head(); let text_layout_details = editor.text_layout_details(cx);
original_columns.insert(selection.id, original_head.column()); editor.transact(cx, |editor, cx| {
motion.expand_selection(map, selection, times, true, &text_layout_details); editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
original_columns.insert(selection.id, original_head.column());
motion.expand_selection(map, selection, times, true, &text_layout_details);
// Motion::NextWordStart on an empty line should delete it. // Motion::NextWordStart on an empty line should delete it.
if let Motion::NextWordStart { if let Motion::NextWordStart {
ignore_punctuation: _, ignore_punctuation: _,
} = motion } = motion
{
if selection.is_empty()
&& map
.buffer_snapshot
.line_len(MultiBufferRow(selection.start.to_point(&map).row))
== 0
{ {
selection.end = map if selection.is_empty()
.buffer_snapshot && map
.clip_point( .buffer_snapshot
Point::new(selection.start.to_point(&map).row + 1, 0), .line_len(MultiBufferRow(selection.start.to_point(&map).row))
Bias::Left, == 0
) {
.to_display_point(map) selection.end = map
} .buffer_snapshot
} .clip_point(
}); Point::new(selection.start.to_point(&map).row + 1, 0),
}); Bias::Left,
copy_selections_content(vim, editor, motion.linewise(), cx); )
editor.insert("", cx); .to_display_point(map)
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if motion.linewise() {
if let Some(column) = original_columns.get(&selection.id) {
*cursor.column_mut() = *column
}
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});
}
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| {
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
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let mut move_selection_start_to_previous_line =
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
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);
} }
};
let range = selection.start.to_offset(map, Bias::Left)
..selection.end.to_offset(map, Bias::Right);
let contains_only_newlines = map
.buffer_chars_at(range.start)
.take_while(|(_, p)| p < &range.end)
.all(|(char, _)| char == '\n')
&& !offset_range.is_empty();
let end_at_newline = map
.buffer_chars_at(range.end)
.next()
.map(|(c, _)| c == '\n')
.unwrap_or(false);
// If expanded range contains only newlines and
// the object is around or sentence, expand to include a newline
// at the end or start
if (around || object == Object::Sentence) && contains_only_newlines {
if end_at_newline {
move_selection_end_to_next_line(map, selection);
} else {
move_selection_start_to_previous_line(map, selection);
} }
} });
// Does post-processing for the trailing newline and EOF
// when not cancelled.
let cancelled = around && selection.start == selection.end;
if object == Object::Paragraph && !cancelled {
// EOF check should be done before including a trailing newline.
if ends_at_eof(map, selection) {
move_selection_start_to_previous_line(map, selection);
}
if end_at_newline {
move_selection_end_to_next_line(map, selection);
}
}
}); });
}); vim.copy_selections_content(editor, motion.linewise(), cx);
copy_selections_content(vim, editor, false, cx); editor.insert("", cx);
editor.insert("", cx);
// Fixup cursor position after the deletion // Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx); editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let mut cursor = selection.head(); let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) { if motion.linewise() {
*cursor.column_mut() = 0; if let Some(column) = original_columns.get(&selection.id) {
} *cursor.column_mut() = *column
cursor = map.clip_point(cursor, Bias::Left); }
selection.collapse_to(cursor, selection.goal) }
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
}); });
}); });
}); });
}); }
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
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let mut move_selection_start_to_previous_line =
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
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);
}
};
let range = selection.start.to_offset(map, Bias::Left)
..selection.end.to_offset(map, Bias::Right);
let contains_only_newlines = map
.buffer_chars_at(range.start)
.take_while(|(_, p)| p < &range.end)
.all(|(char, _)| char == '\n')
&& !offset_range.is_empty();
let end_at_newline = map
.buffer_chars_at(range.end)
.next()
.map(|(c, _)| c == '\n')
.unwrap_or(false);
// If expanded range contains only newlines and
// the object is around or sentence, expand to include a newline
// at the end or start
if (around || object == Object::Sentence) && contains_only_newlines {
if end_at_newline {
move_selection_end_to_next_line(map, selection);
} else {
move_selection_start_to_previous_line(map, selection);
}
}
// Does post-processing for the trailing newline and EOF
// when not cancelled.
let cancelled = around && selection.start == selection.end;
if object == Object::Paragraph && !cancelled {
// EOF check should be done before including a trailing newline.
if ends_at_eof(map, selection) {
move_selection_start_to_previous_line(map, selection);
}
if end_at_newline {
move_selection_end_to_next_line(map, selection);
}
}
});
});
vim.copy_selections_content(editor, false, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
*cursor.column_mut() = 0;
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});
}
} }
fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) { fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {

View File

@ -1,10 +1,9 @@
use std::ops::Range; use std::ops::Range;
use editor::{scroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint}; use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint};
use gpui::{impl_actions, ViewContext, WindowContext}; use gpui::{impl_actions, ViewContext};
use language::{Bias, Point}; use language::{Bias, Point};
use serde::Deserialize; use serde::Deserialize;
use workspace::Workspace;
use crate::{state::Mode, Vim}; use crate::{state::Mode, Vim};
@ -24,92 +23,90 @@ struct Decrement {
impl_actions!(vim, [Increment, Decrement]); impl_actions!(vim, [Increment, Decrement]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_: &mut Workspace, action: &Increment, cx| { Vim::action(editor, cx, |vim, action: &Increment, cx| {
Vim::update(cx, |vim, cx| { vim.record_current_action(cx);
vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1);
let count = vim.take_count(cx).unwrap_or(1); let step = if action.step { 1 } else { 0 };
let step = if action.step { 1 } else { 0 }; vim.increment(count as i32, step, cx)
increment(vim, count as i32, step, cx)
})
}); });
workspace.register_action(|_: &mut Workspace, action: &Decrement, cx| { Vim::action(editor, cx, |vim, action: &Decrement, cx| {
Vim::update(cx, |vim, cx| { vim.record_current_action(cx);
vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1);
let count = vim.take_count(cx).unwrap_or(1); let step = if action.step { -1 } else { 0 };
let step = if action.step { -1 } else { 0 }; vim.increment(count as i32 * -1, step, cx)
increment(vim, count as i32 * -1, step, cx)
})
}); });
} }
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { impl Vim {
vim.store_visual_marks(cx); fn increment(&mut self, mut delta: i32, step: i32, cx: &mut ViewContext<Self>) {
vim.update_active_editor(cx, |vim, editor, cx| { self.store_visual_marks(cx);
let mut edits = Vec::new(); self.update_editor(cx, |vim, editor, cx| {
let mut new_anchors = Vec::new(); 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() {
new_anchors.push((true, snapshot.anchor_before(selection.start)))
}
}
for row in selection.start.row..=selection.end.row {
let start = if row == selection.start.row {
selection.start
} else {
Point::new(row, 0)
};
if let Some((range, num, radix)) = find_number(&snapshot, start) {
if let Ok(val) = i32::from_str_radix(&num, radix) {
let result = val + delta;
delta += step;
let replace = match radix {
10 => format!("{}", result),
16 => {
if num.to_ascii_lowercase() == num {
format!("{:x}", result)
} else {
format!("{:X}", result)
}
}
2 => format!("{:b}", result),
_ => unreachable!(),
};
edits.push((range.clone(), replace));
}
if selection.is_empty() {
new_anchors.push((false, snapshot.anchor_after(range.end)))
}
} else {
if selection.is_empty() {
new_anchors.push((true, snapshot.anchor_after(start)))
}
}
}
}
editor.transact(cx, |editor, cx| {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { for selection in editor.selections.all_adjusted(cx) {
let mut new_ranges = Vec::new(); if !selection.is_empty() {
for (visual, anchor) in new_anchors.iter() { if vim.mode != Mode::VisualBlock || new_anchors.is_empty() {
let mut point = anchor.to_point(&snapshot); new_anchors.push((true, snapshot.anchor_before(selection.start)))
if !*visual && point.column > 0 {
point.column -= 1;
point = snapshot.clip_point(point, Bias::Left)
} }
new_ranges.push(point..point);
} }
s.select_ranges(new_ranges) for row in selection.start.row..=selection.end.row {
}) let start = if row == selection.start.row {
selection.start
} else {
Point::new(row, 0)
};
if let Some((range, num, radix)) = find_number(&snapshot, start) {
if let Ok(val) = i32::from_str_radix(&num, radix) {
let result = val + delta;
delta += step;
let replace = match radix {
10 => format!("{}", result),
16 => {
if num.to_ascii_lowercase() == num {
format!("{:x}", result)
} else {
format!("{:X}", result)
}
}
2 => format!("{:b}", result),
_ => unreachable!(),
};
edits.push((range.clone(), replace));
}
if selection.is_empty() {
new_anchors.push((false, snapshot.anchor_after(range.end)))
}
} else {
if selection.is_empty() {
new_anchors.push((true, snapshot.anchor_after(start)))
}
}
}
}
editor.transact(cx, |editor, cx| {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut new_ranges = Vec::new();
for (visual, anchor) in new_anchors.iter() {
let mut point = anchor.to_point(&snapshot);
if !*visual && point.column > 0 {
point.column -= 1;
point = snapshot.clip_point(point, Bias::Left)
}
new_ranges.push(point..point);
}
s.select_ranges(new_ranges)
})
});
}); });
}); self.switch_mode(Mode::Normal, true, cx)
vim.switch_mode(Mode::Normal, true, cx) }
} }
fn find_number( fn find_number(

View File

@ -1,78 +1,80 @@
use crate::{motion::Motion, object::Object, Vim}; use crate::{motion::Motion, object::Object, Vim};
use collections::HashMap; use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Bias}; use editor::{display_map::ToDisplayPoint, Bias};
use gpui::WindowContext;
use language::SelectionGoal; use language::SelectionGoal;
use ui::ViewContext;
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub(super) enum IndentDirection { pub(crate) enum IndentDirection {
In, In,
Out, Out,
} }
pub fn indent_motion( impl Vim {
vim: &mut Vim, pub(crate) fn indent_motion(
motion: Motion, &mut self,
times: Option<usize>, motion: Motion,
dir: IndentDirection, times: Option<usize>,
cx: &mut WindowContext, dir: IndentDirection,
) { cx: &mut ViewContext<Self>,
vim.stop_recording(); ) {
vim.update_active_editor(cx, |_, editor, cx| { self.stop_recording(cx);
let text_layout_details = editor.text_layout_details(cx); self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx);
let mut selection_starts: HashMap<_, _> = Default::default(); editor.transact(cx, |editor, cx| {
editor.change_selections(None, cx, |s| { let mut selection_starts: HashMap<_, _> = Default::default();
s.move_with(|map, selection| { editor.change_selections(None, cx, |s| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); s.move_with(|map, selection| {
selection_starts.insert(selection.id, anchor); let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
motion.expand_selection(map, selection, times, false, &text_layout_details); selection_starts.insert(selection.id, anchor);
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
}); });
}); if dir == IndentDirection::In {
if dir == IndentDirection::In { editor.indent(&Default::default(), cx);
editor.indent(&Default::default(), cx); } else {
} else { editor.outdent(&Default::default(), cx);
editor.outdent(&Default::default(), cx); }
} editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = selection_starts.remove(&selection.id).unwrap();
let anchor = selection_starts.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
}
pub fn indent_object( pub(crate) fn indent_object(
vim: &mut Vim, &mut self,
object: Object, object: Object,
around: bool, around: bool,
dir: IndentDirection, dir: IndentDirection,
cx: &mut WindowContext, cx: &mut ViewContext<Self>,
) { ) {
vim.stop_recording(); self.stop_recording(cx);
vim.update_active_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
let mut original_positions: HashMap<_, _> = Default::default(); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor); original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around); object.expand_selection(map, selection, around);
});
}); });
}); if dir == IndentDirection::In {
if dir == IndentDirection::In { editor.indent(&Default::default(), cx);
editor.indent(&Default::default(), cx); } else {
} else { editor.outdent(&Default::default(), cx);
editor.outdent(&Default::default(), cx); }
} editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = original_positions.remove(&selection.id).unwrap();
let anchor = original_positions.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
} }

View File

@ -6,7 +6,7 @@ use editor::{
scroll::Autoscroll, scroll::Autoscroll,
Anchor, Bias, DisplayPoint, Anchor, Bias, DisplayPoint,
}; };
use gpui::WindowContext; use gpui::ViewContext;
use language::SelectionGoal; use language::SelectionGoal;
use crate::{ use crate::{
@ -15,56 +15,62 @@ use crate::{
Vim, Vim,
}; };
pub fn create_mark(vim: &mut Vim, text: Arc<str>, tail: bool, cx: &mut WindowContext) { impl Vim {
let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| { pub fn create_mark(&mut self, text: Arc<str>, tail: bool, cx: &mut ViewContext<Self>) {
editor let Some(anchors) = self.update_editor(cx, |_, editor, _| {
.selections editor
.disjoint_anchors() .selections
.iter() .disjoint_anchors()
.map(|s| if tail { s.tail() } else { s.head() }) .iter()
.collect::<Vec<_>>() .map(|s| if tail { s.tail() } else { s.head() })
}) else { .collect::<Vec<_>>()
return; }) 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
let mut starts = vec![]; // mode without the default selection behavior.
let mut ends = vec![]; pub(crate) fn store_visual_marks(&mut self, cx: &mut ViewContext<Self>) {
let mut reversed = vec![]; if self.mode.is_visual() {
self.create_visual_marks(self.mode, cx);
vim.update_active_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
for selection in selections {
let end = movement::saturating_left(&map, selection.end);
ends.push(
map.buffer_snapshot
.anchor_before(end.to_offset(&map, Bias::Left)),
);
starts.push(
map.buffer_snapshot
.anchor_after(selection.start.to_offset(&map, Bias::Right)),
);
reversed.push(selection.reversed)
} }
}); }
vim.update_state(|state| { pub(crate) fn create_visual_marks(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
state.marks.insert("<".to_string(), starts); let mut starts = vec![];
state.marks.insert(">".to_string(), ends); let mut ends = vec![];
state.stored_visual_mode.replace((mode, reversed)); let mut reversed = vec![];
});
vim.clear_operator(cx);
}
pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) { self.update_editor(cx, |_, editor, cx| {
let anchors = Vim::update(cx, |vim, cx| { let (map, selections) = editor.selections.all_display(cx);
vim.pop_operator(cx); for selection in selections {
let end = movement::saturating_left(&map, selection.end);
ends.push(
map.buffer_snapshot
.anchor_before(end.to_offset(&map, Bias::Left)),
);
starts.push(
map.buffer_snapshot
.anchor_after(selection.start.to_offset(&map, Bias::Right)),
);
reversed.push(selection.reversed)
}
});
match &*text { self.marks.insert("<".to_string(), starts);
"{" | "}" => vim.update_active_editor(cx, |_, editor, cx| { self.marks.insert(">".to_string(), ends);
self.stored_visual_mode.replace((mode, reversed));
self.clear_operator(cx);
}
pub fn jump(&mut self, text: Arc<str>, line: bool, cx: &mut ViewContext<Self>) {
self.pop_operator(cx);
let anchors = match &*text {
"{" | "}" => self.update_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx); let (map, selections) = editor.selections.all_display(cx);
selections selections
.into_iter() .into_iter()
@ -79,28 +85,26 @@ pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
}) })
.collect::<Vec<Anchor>>() .collect::<Vec<Anchor>>()
}), }),
"." => vim.state().change_list.last().cloned(), "." => self.change_list.last().cloned(),
_ => vim.state().marks.get(&*text).cloned(), _ => self.marks.get(&*text).cloned(),
} };
});
let Some(anchors) = anchors else { return }; 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 is_active_operator {
if let Some(anchor) = anchors.last() { if let Some(anchor) = anchors.last() {
motion::motion( self.motion(
Motion::Jump { Motion::Jump {
anchor: *anchor, anchor: *anchor,
line, line,
}, },
cx, cx,
) )
} }
return; return;
} else { } else {
Vim::update(cx, |vim, cx| { self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
let map = editor.snapshot(cx); let map = editor.snapshot(cx);
let mut ranges: Vec<Range<Anchor>> = Vec::new(); let mut ranges: Vec<Range<Anchor>> = Vec::new();
for mut anchor in anchors { 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) 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 gpui::{impl_actions, ViewContext};
use language::{Bias, SelectionGoal}; use language::{Bias, SelectionGoal};
use serde::Deserialize; use serde::Deserialize;
use workspace::Workspace;
use crate::{ use crate::{
normal::yank::copy_selections_content,
state::{Mode, Register}, state::{Mode, Register},
Vim, Vim,
}; };
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Paste { pub struct Paste {
#[serde(default)] #[serde(default)]
before: bool, before: bool,
#[serde(default)] #[serde(default)]
@ -23,37 +21,34 @@ struct Paste {
impl_actions!(vim, [Paste]); impl_actions!(vim, [Paste]);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { impl Vim {
workspace.register_action(paste); 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>) { self.update_editor(cx, |vim, editor, cx| {
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| {
let text_layout_details = editor.text_layout_details(cx); let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, 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 { let Some(Register {
text, text,
clipboard_selections, clipboard_selections,
}) = vim }) = Vim::update_globals(cx, |globals, cx| {
.read_register(selected_register, Some(editor), cx) globals.read_register(selected_register, Some(editor), cx)
.filter(|reg| !reg.text.is_empty()) })
.filter(|reg| !reg.text.is_empty())
else { else {
return; return;
}; };
let clipboard_selections = clipboard_selections 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() { if !action.preserve_clipboard && vim.mode.is_visual() {
copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx); vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, cx);
} }
let (display_map, current_selections) = editor.selections.all_adjusted_display(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() .first()
.map(|selection| selection.first_line_indent) .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 edits = Vec::new();
let mut new_selections = Vec::new(); let mut new_selections = Vec::new();
@ -121,7 +116,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
} else { } else {
to_insert = "\n".to_owned() + &to_insert; 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"; 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) let point_range = display_range.start.to_point(&display_map)
..display_range.end.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) display_map.buffer_snapshot.anchor_before(point_range.start)
} else { } else {
display_map.buffer_snapshot.anchor_after(point_range.end) 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) cursor = movement::saturating_left(map, cursor)
} }
cursors.push(cursor); cursors.push(cursor);
if vim.state().mode == Mode::VisualBlock { if vim.mode == Mode::VisualBlock {
break; 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)] #[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::{ use crate::{
insert::NormalBefore, insert::NormalBefore,
motion::Motion, motion::Motion,
state::{Mode, Operator, RecordedSelection, ReplayableAction}, state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
visual::visual_motion,
Vim, Vim,
}; };
use editor::Editor;
use gpui::{actions, Action, ViewContext, WindowContext}; use gpui::{actions, Action, ViewContext, WindowContext};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; 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>) { pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| { Vim::action(editor, cx, |vim, _: &EndRepeat, cx| {
Vim::update(cx, |vim, cx| { Vim::globals(cx).dot_replaying = false;
vim.workspace_state.dot_replaying = false; vim.switch_mode(Mode::Normal, false, cx)
vim.switch_mode(Mode::Normal, false, cx)
});
}); });
workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false)); Vim::action(editor, cx, |vim, _: &Repeat, cx| vim.repeat(false, cx));
workspace.register_action(|_: &mut Workspace, _: &ToggleRecord, cx| {
Vim::update(cx, |vim, cx| { Vim::action(editor, cx, |vim, _: &ToggleRecord, cx| {
if let Some(char) = vim.workspace_state.recording_register.take() { let globals = Vim::globals(cx);
vim.workspace_state.last_recorded_register = Some(char) if let Some(char) = globals.recording_register.take() {
} else { globals.last_recorded_register = Some(char)
vim.push_operator(Operator::RecordRegister, cx); } else {
} vim.push_operator(Operator::RecordRegister, cx);
}) }
}); });
workspace.register_action(|_: &mut Workspace, _: &ReplayLastRecording, cx| { Vim::action(editor, cx, |vim, _: &ReplayLastRecording, cx| {
let Some(register) = Vim::read(cx).workspace_state.last_recorded_register else { let Some(register) = Vim::globals(cx).last_recorded_register else {
return; return;
}; };
replay_register(register, cx) vim.replay_register(register, cx)
}); });
} }
@ -116,54 +114,60 @@ impl Replayer {
lock.ix += 1; lock.ix += 1;
drop(lock); drop(lock);
let Some(action) = action else { let Some(action) = action else {
Vim::update(cx, |vim, _| vim.workspace_state.replayer.take()); Vim::globals(cx).replayer.take();
return; return;
}; };
match action { match action {
ReplayableAction::Action(action) => { ReplayableAction::Action(action) => {
if should_replay(&*action) { if should_replay(&*action) {
cx.dispatch_action(action.boxed_clone()); 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 { ReplayableAction::Insertion {
text, text,
utf16_range_to_replace, utf16_range_to_replace,
} => { } => {
if let Some(editor) = Vim::read(cx).active_editor.clone() { cx.window_handle()
editor .update(cx, |handle, cx| {
.update(cx, |editor, 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) editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
}) })
.log_err(); })
} .log_err();
} }
} }
cx.defer(move |cx| self.next(cx)); cx.defer(move |cx| self.next(cx));
} }
} }
pub(crate) fn record_register(register: char, cx: &mut WindowContext) { impl Vim {
Vim::update(cx, |vim, cx| { pub(crate) fn record_register(&mut self, register: char, cx: &mut ViewContext<Self>) {
vim.workspace_state.recording_register = Some(register); let globals = Vim::globals(cx);
vim.workspace_state.recordings.remove(&register); globals.recording_register = Some(register);
vim.workspace_state.ignore_current_insertion = true; globals.recordings.remove(&register);
vim.clear_operator(cx) globals.ignore_current_insertion = true;
}) self.clear_operator(cx)
} }
pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) { pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { let mut count = self.take_count(cx).unwrap_or(1);
let mut count = vim.take_count(cx).unwrap_or(1); self.clear_operator(cx);
vim.clear_operator(cx);
let globals = Vim::globals(cx);
if register == '@' { if register == '@' {
let Some(last) = vim.workspace_state.last_replayed_register else { let Some(last) = globals.last_replayed_register else {
return; return;
}; };
register = last; register = last;
} }
let Some(actions) = vim.workspace_state.recordings.get(&register) else { let Some(actions) = globals.recordings.get(&register) else {
return; return;
}; };
@ -173,206 +177,148 @@ pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) {
count -= 1 count -= 1
} }
vim.workspace_state.last_replayed_register = Some(register); globals.last_replayed_register = Some(register);
let mut replayer = globals
vim.workspace_state
.replayer .replayer
.get_or_insert_with(|| Replayer::new()) .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) { pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext<Self>) {
let Some((mut actions, selection)) = Vim::update(cx, |vim, cx| { let count = self.take_count(cx);
let actions = vim.workspace_state.recorded_actions.clone(); let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
if actions.is_empty() { let actions = globals.recorded_actions.clone();
return None; 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 { .. } => { if globals.replayer.is_none() {
vim.workspace_state.recorded_count = None; if let Some(recording_register) = globals.recording_register {
vim.switch_mode(Mode::VisualLine, false, cx) globals
} .recordings
RecordedSelection::VisualBlock { .. } => { .entry(recording_register)
vim.workspace_state.recorded_count = None; .or_default()
vim.switch_mode(Mode::VisualBlock, false, cx) .push(ReplayableAction::Action(Repeat.boxed_clone()));
}
RecordedSelection::None => {
if let Some(count) = count {
vim.workspace_state.recorded_count = Some(count);
} }
} }
}
if vim.workspace_state.replayer.is_none() { let mut mode = None;
if let Some(recording_register) = vim.workspace_state.recording_register { let selection = globals.recorded_selection.clone();
vim.workspace_state match selection {
.recordings RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
.entry(recording_register) globals.recorded_count = None;
.or_default() mode = Some(Mode::Visual);
.push(ReplayableAction::Action(Repeat.boxed_clone())); }
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)
} }
Some((actions, selection)) match selection {
}) else { RecordedSelection::SingleLine { cols } => {
return; if cols > 1 {
}; self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
}
match selection {
RecordedSelection::SingleLine { cols } => {
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
} }
} RecordedSelection::Visual { rows, cols } => {
RecordedSelection::Visual { rows, cols } => { self.visual_motion(
visual_motion( Motion::Down {
Motion::Down { display_lines: false,
display_lines: false, },
}, Some(rows as usize),
Some(rows as usize), cx,
cx, );
); self.visual_motion(
visual_motion( Motion::StartOfLine {
Motion::StartOfLine { display_lines: false,
display_lines: false, },
}, None,
None, cx,
cx, );
); if cols > 1 {
if cols > 1 { self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
visual_motion(Motion::Right, Some(cols as usize - 1), cx) }
} }
} RecordedSelection::VisualBlock { rows, cols } => {
RecordedSelection::VisualBlock { rows, cols } => { self.visual_motion(
visual_motion( Motion::Down {
Motion::Down { display_lines: false,
display_lines: false, },
}, Some(rows as usize),
Some(rows as usize), cx,
cx, );
); if cols > 1 {
if cols > 1 { self.visual_motion(Motion::Right, Some(cols as usize - 1), cx);
visual_motion(Motion::Right, Some(cols as usize - 1), cx); }
} }
} RecordedSelection::VisualLine { rows } => {
RecordedSelection::VisualLine { rows } => { self.visual_motion(
visual_motion( Motion::Down {
Motion::Down { display_lines: false,
display_lines: false, },
}, Some(rows as usize),
Some(rows as usize), cx,
cx, );
);
}
RecordedSelection::None => {}
}
// insert internally uses repeat to handle counts
// vim doesn't treat 3a1 as though you literally repeated a1
// 3 times, instead it inserts the content thrice at the insert position.
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
if let Some(ReplayableAction::Action(action)) = actions.last() {
if NormalBefore.partial_eq(&**action) {
actions.pop();
} }
RecordedSelection::None => {}
} }
let mut new_actions = actions.clone(); // insert internally uses repeat to handle counts
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); // vim doesn't treat 3a1 as though you literally repeated a1
// 3 times, instead it inserts the content thrice at the insert position.
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
if let Some(ReplayableAction::Action(action)) = actions.last() {
if NormalBefore.partial_eq(&**action) {
actions.pop();
}
}
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); let mut new_actions = actions.clone();
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
// if we came from insert mode we're just doing repetitions 2 onwards. let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
if from_insert_mode {
count -= 1; // if we came from insert mode we're just doing repetitions 2 onwards.
new_actions[0] = actions[0].clone(); if from_insert_mode {
count -= 1;
new_actions[0] = actions[0].clone();
}
for _ in 1..count {
new_actions.append(actions.clone().as_mut());
}
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
actions = new_actions;
} }
for _ in 1..count { actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
new_actions.append(actions.clone().as_mut());
}
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
actions = new_actions;
}
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); let globals = Vim::globals(cx);
globals.dot_replaying = true;
Vim::update(cx, |vim, cx| { let mut replayer = globals
vim.workspace_state.dot_replaying = true;
vim.workspace_state
.replayer .replayer
.get_or_insert_with(|| Replayer::new()) .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)] #[cfg(test)]

View File

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

View File

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

View File

@ -1,85 +1,87 @@
use editor::movement; use editor::{movement, Editor};
use gpui::{actions, ViewContext, WindowContext}; use gpui::{actions, ViewContext};
use language::Point; 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]); actions!(vim, [Substitute, SubstituteLine]);
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::action(editor, cx, |vim, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| { vim.start_recording(cx);
vim.start_recording(cx); let count = vim.take_count(cx);
let count = vim.take_count(cx); vim.substitute(count, vim.mode == Mode::VisualLine, cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
}); });
workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| { Vim::action(editor, cx, |vim, _: &SubstituteLine, cx| {
Vim::update(cx, |vim, cx| { vim.start_recording(cx);
vim.start_recording(cx); if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { vim.switch_mode(Mode::VisualLine, false, cx)
vim.switch_mode(Mode::VisualLine, false, cx) }
} let count = vim.take_count(cx);
let count = vim.take_count(cx); vim.substitute(count, true, cx)
substitute(vim, count, true, cx)
})
}); });
} }
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) { impl Vim {
vim.store_visual_marks(cx); pub fn substitute(
vim.update_active_editor(cx, |vim, editor, cx| { &mut self,
editor.set_clip_at_line_ends(false, cx); count: Option<usize>,
editor.transact(cx, |editor, cx| { line_mode: bool,
let text_layout_details = editor.text_layout_details(cx); cx: &mut ViewContext<Self>,
editor.change_selections(None, cx, |s| { ) {
s.move_with(|map, selection| { self.store_visual_marks(cx);
if selection.start == selection.end { self.update_editor(cx, |vim, editor, cx| {
Motion::Right.expand_selection( editor.set_clip_at_line_ends(false, cx);
map, editor.transact(cx, |editor, cx| {
selection, let text_layout_details = editor.text_layout_details(cx);
count, editor.change_selections(None, cx, |s| {
true, s.move_with(|map, selection| {
&text_layout_details, if selection.start == selection.end {
); Motion::Right.expand_selection(
} map,
if line_mode { selection,
// in Visual mode when the selection contains the newline at the end count,
// of the line, we should exclude it. true,
if !selection.is_empty() && selection.end.column() == 0 { &text_layout_details,
selection.end = movement::left(map, selection.end); );
} }
Motion::CurrentLine.expand_selection( if line_mode {
map, // in Visual mode when the selection contains the newline at the end
selection, // of the line, we should exclude it.
None, if !selection.is_empty() && selection.end.column() == 0 {
false, selection.end = movement::left(map, selection.end);
&text_layout_details, }
); Motion::CurrentLine.expand_selection(
if let Some((point, _)) = (Motion::FirstNonWhitespace { map,
display_lines: false, selection,
}) None,
.move_point( false,
map, &text_layout_details,
selection.start, );
selection.goal, if let Some((point, _)) = (Motion::FirstNonWhitespace {
None, display_lines: false,
&text_layout_details, })
) { .move_point(
selection.start = point; map,
selection.start,
selection.goal,
None,
&text_layout_details,
) {
selection.start = point;
}
} }
} })
}) });
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);
}); });
copy_selections_content(vim, 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);
}); });
}); self.switch_mode(Mode::Insert, true, cx);
vim.switch_mode(Mode::Insert, true, cx); }
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,57 +1,64 @@
use crate::{motion::Motion, object::Object, Vim}; use crate::{motion::Motion, object::Object, Vim};
use collections::HashMap; use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Bias}; use editor::{display_map::ToDisplayPoint, Bias};
use gpui::WindowContext;
use language::SelectionGoal; use language::SelectionGoal;
use ui::ViewContext;
pub fn toggle_comments_motion( impl Vim {
vim: &mut Vim, pub fn toggle_comments_motion(
motion: Motion, &mut self,
times: Option<usize>, motion: Motion,
cx: &mut WindowContext, times: Option<usize>,
) { cx: &mut ViewContext<Self>,
vim.stop_recording(); ) {
vim.update_active_editor(cx, |_, editor, cx| { self.stop_recording(cx);
let text_layout_details = editor.text_layout_details(cx); self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx);
let mut selection_starts: HashMap<_, _> = Default::default(); editor.transact(cx, |editor, cx| {
editor.change_selections(None, cx, |s| { let mut selection_starts: HashMap<_, _> = Default::default();
s.move_with(|map, selection| { editor.change_selections(None, cx, |s| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); s.move_with(|map, selection| {
selection_starts.insert(selection.id, anchor); let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
motion.expand_selection(map, selection, times, false, &text_layout_details); selection_starts.insert(selection.id, anchor);
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
}); });
}); editor.toggle_comments(&Default::default(), cx);
editor.toggle_comments(&Default::default(), cx); editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = selection_starts.remove(&selection.id).unwrap();
let anchor = selection_starts.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
}
pub fn toggle_comments_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { pub fn toggle_comments_object(
vim.stop_recording(); &mut self,
vim.update_active_editor(cx, |_, editor, cx| { object: Object,
editor.transact(cx, |editor, cx| { around: bool,
let mut original_positions: HashMap<_, _> = Default::default(); cx: &mut ViewContext<Self>,
editor.change_selections(None, cx, |s| { ) {
s.move_with(|map, selection| { self.stop_recording(cx);
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); self.update_editor(cx, |_, editor, cx| {
original_positions.insert(selection.id, anchor); editor.transact(cx, |editor, cx| {
object.expand_selection(map, selection, around); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around);
});
}); });
}); editor.toggle_comments(&Default::default(), cx);
editor.toggle_comments(&Default::default(), cx); editor.change_selections(None, cx, |s| {
editor.change_selections(None, cx, |s| { s.move_with(|map, selection| {
s.move_with(|map, selection| { let anchor = original_positions.remove(&selection.id).unwrap();
let anchor = original_positions.remove(&selection.id).unwrap(); selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); });
}); });
}); });
}); });
}); }
} }

View File

@ -8,181 +8,187 @@ use crate::{
}; };
use collections::HashMap; use collections::HashMap;
use editor::{ClipboardSelection, Editor}; use editor::{ClipboardSelection, Editor};
use gpui::WindowContext; use gpui::ViewContext;
use language::Point; use language::Point;
use multi_buffer::MultiBufferRow; 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| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
original_positions.insert(selection.id, original_position);
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
yank_selections_content(vim, editor, motion.linewise(), cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});
});
});
}
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.update_active_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();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around);
original_positions.insert(selection.id, original_position);
});
});
yank_selections_content(vim, editor, false, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});
});
});
}
pub fn yank_selections_content(
vim: &mut Vim,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
copy_selections_content_internal(vim, editor, linewise, true, cx);
}
pub fn copy_selections_content(
vim: &mut Vim,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
copy_selections_content_internal(vim, editor, linewise, false, cx);
}
struct HighlightOnYank; struct HighlightOnYank;
fn copy_selections_content_internal( impl Vim {
vim: &mut Vim, pub fn yank_motion(
editor: &mut Editor, &mut self,
linewise: bool, motion: Motion,
is_yank: bool, times: Option<usize>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Self>,
) { ) {
let selections = editor.selections.all_adjusted(cx); self.update_editor(cx, |vim, editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx); let text_layout_details = editor.text_layout_details(cx);
let mut text = String::new(); editor.transact(cx, |editor, cx| {
let mut clipboard_selections = Vec::with_capacity(selections.len()); editor.set_clip_at_line_ends(false, cx);
let mut ranges_to_highlight = Vec::new(); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
original_positions.insert(selection.id, original_position);
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
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();
selection.collapse_to(head, goal);
});
});
});
});
}
vim.update_state(|state| { pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
state.marks.insert( 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();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around);
original_positions.insert(selection.id, original_position);
});
});
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();
selection.collapse_to(head, goal);
});
});
});
});
}
pub fn yank_selections_content(
&mut self,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
self.copy_selections_content_internal(editor, linewise, true, cx);
}
pub fn copy_selections_content(
&mut self,
editor: &mut Editor,
linewise: bool,
cx: &mut ViewContext<Editor>,
) {
self.copy_selections_content_internal(editor, linewise, false, cx);
}
fn copy_selections_content_internal(
&mut self,
editor: &mut Editor,
linewise: bool,
is_yank: bool,
cx: &mut ViewContext<Editor>,
) {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
let mut ranges_to_highlight = Vec::new();
self.marks.insert(
"[".to_string(), "[".to_string(),
selections selections
.iter() .iter()
.map(|s| buffer.anchor_before(s.start)) .map(|s| buffer.anchor_before(s.start))
.collect(), .collect(),
); );
state.marks.insert( self.marks.insert(
"]".to_string(), "]".to_string(),
selections selections
.iter() .iter()
.map(|s| buffer.anchor_after(s.end)) .map(|s| buffer.anchor_after(s.end))
.collect(), .collect(),
) );
});
{ {
let mut is_first = true; let mut is_first = true;
for selection in selections.iter() { for selection in selections.iter() {
let mut start = selection.start; let mut start = selection.start;
let end = selection.end; let end = selection.end;
if is_first { if is_first {
is_first = false; is_first = false;
} else { } else {
text.push_str("\n"); text.push_str("\n");
}
let initial_len = text.len();
// if the file does not end with \n, and our line-mode selection ends on
// that line, we will have expanded the start of the selection to ensure it
// contains a newline (so that delete works as expected). We undo that change
// here.
let is_last_line = linewise
&& end.row == buffer.max_buffer_row().0
&& buffer.max_point().column > 0
&& start.row < buffer.max_buffer_row().0
&& start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
if is_last_line {
start = Point::new(start.row + 1, 0);
}
let start_anchor = buffer.anchor_after(start);
let end_anchor = buffer.anchor_before(end);
ranges_to_highlight.push(start_anchor..end_anchor);
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
if is_last_line {
text.push_str("\n");
}
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
is_entire_line: linewise,
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
});
} }
let initial_len = text.len();
// if the file does not end with \n, and our line-mode selection ends on
// that line, we will have expanded the start of the selection to ensure it
// contains a newline (so that delete works as expected). We undo that change
// here.
let is_last_line = linewise
&& end.row == buffer.max_buffer_row().0
&& buffer.max_point().column > 0
&& start.row < buffer.max_buffer_row().0
&& start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
if is_last_line {
start = Point::new(start.row + 1, 0);
}
let start_anchor = buffer.anchor_after(start);
let end_anchor = buffer.anchor_before(end);
ranges_to_highlight.push(start_anchor..end_anchor);
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
if is_last_line {
text.push_str("\n");
}
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
is_entire_line: linewise,
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
});
} }
}
let selected_register = vim.update_state(|state| state.selected_register.take()); let selected_register = self.selected_register.take();
vim.write_registers( Vim::update_globals(cx, |globals, cx| {
Register { globals.write_registers(
text: text.into(), Register {
clipboard_selections: Some(clipboard_selections), text: text.into(),
}, clipboard_selections: Some(clipboard_selections),
selected_register, },
is_yank, selected_register,
linewise, is_yank,
cx, linewise,
); cx,
)
});
if !is_yank || vim.state().mode == Mode::Visual { if !is_yank || self.mode == Mode::Visual {
return; return;
} }
editor.highlight_background::<HighlightOnYank>( editor.highlight_background::<HighlightOnYank>(
&ranges_to_highlight, &ranges_to_highlight,
|colors| colors.editor_document_highlight_read_background, |colors| colors.editor_document_highlight_read_background,
cx, cx,
); );
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
cx.background_executor() cx.background_executor()
.timer(Duration::from_millis(200)) .timer(Duration::from_millis(200))
.await; .await;
this.update(&mut cx, |editor, cx| { this.update(&mut cx, |editor, cx| {
editor.clear_background_highlights::<HighlightOnYank>(cx) editor.clear_background_highlights::<HighlightOnYank>(cx)
})
.ok();
}) })
.ok(); .detach();
}) }
.detach();
} }

View File

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

View File

@ -3,38 +3,33 @@ use crate::{
state::Mode, state::Mode,
Vim, Vim,
}; };
use editor::{display_map::ToDisplayPoint, Bias, ToPoint}; use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint};
use gpui::{actions, ViewContext, WindowContext}; use gpui::{actions, ViewContext};
use language::{AutoindentMode, Point}; use language::{AutoindentMode, Point};
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use workspace::Workspace;
actions!(vim, [ToggleReplace, UndoReplace]); actions!(vim, [ToggleReplace, UndoReplace]);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext<Workspace>| { Vim::action(editor, cx, |vim, _: &ToggleReplace, cx| {
Vim::update(cx, |vim, cx| { vim.replacements = vec![];
vim.update_state(|state| state.replacements = vec![]); vim.start_recording(cx);
vim.start_recording(cx); vim.switch_mode(Mode::Replace, false, cx);
vim.switch_mode(Mode::Replace, false, cx);
});
}); });
workspace.register_action(|_, _: &UndoReplace, cx: &mut ViewContext<Workspace>| { Vim::action(editor, cx, |vim, _: &UndoReplace, cx| {
Vim::update(cx, |vim, cx| { if vim.mode != Mode::Replace {
if vim.state().mode != Mode::Replace { return;
return; }
} let count = vim.take_count(cx);
let count = vim.take_count(cx); vim.undo_replace(count, cx)
undo_replace(vim, count, cx)
});
}); });
} }
pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) { impl Vim {
Vim::update(cx, |vim, cx| { pub(crate) fn multi_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
vim.update_active_editor(cx, |vim, editor, cx| { self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(cx); let map = editor.snapshot(cx);
@ -58,11 +53,7 @@ pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
.buffer_snapshot .buffer_snapshot
.text_for_range(replace_range.clone()) .text_for_range(replace_range.clone())
.collect(); .collect();
vim.update_state(|state| { vim.replacements.push((replace_range.clone(), current_text));
state
.replacements
.push((replace_range.clone(), current_text))
});
(replace_range, text.clone()) (replace_range, text.clone())
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -83,58 +74,56 @@ pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
editor.set_clip_at_line_ends(true, cx); editor.set_clip_at_line_ends(true, cx);
}); });
}); });
}); }
}
fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContext) { fn undo_replace(&mut self, maybe_times: Option<usize>, cx: &mut ViewContext<Self>) {
vim.update_active_editor(cx, |vim, editor, cx| { self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(cx); let map = editor.snapshot(cx);
let selections = editor.selections.all::<Point>(cx); let selections = editor.selections.all::<Point>(cx);
let mut new_selections = vec![]; let mut new_selections = vec![];
let edits: Vec<(Range<Point>, String)> = selections let edits: Vec<(Range<Point>, String)> = selections
.into_iter() .into_iter()
.filter_map(|selection| { .filter_map(|selection| {
let end = selection.head(); let end = selection.head();
let start = motion::backspace( let start = motion::backspace(
&map, &map,
end.to_display_point(&map), end.to_display_point(&map),
maybe_times.unwrap_or(1), maybe_times.unwrap_or(1),
) )
.to_point(&map); .to_point(&map);
new_selections.push( new_selections.push(
map.buffer_snapshot.anchor_before(start) map.buffer_snapshot.anchor_before(start)
..map.buffer_snapshot.anchor_before(start), ..map.buffer_snapshot.anchor_before(start),
); );
let mut undo = None; let mut undo = None;
let edit_range = start..end; 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 if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
&& range.end.to_point(&map.buffer_snapshot) >= edit_range.end && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
{ {
undo = Some(inverse.clone()); undo = Some(inverse.clone());
vim.update_state(|state| { vim.replacements.remove(vim.replacements.len() - i - 1);
state.replacements.remove(state.replacements.len() - i - 1); break;
}); }
break;
} }
} Some((edit_range, undo?))
Some((edit_range, undo?)) })
}) .collect::<Vec<_>>();
.collect::<Vec<_>>();
editor.buffer().update(cx, |buffer, cx| { editor.buffer().update(cx, |buffer, cx| {
buffer.edit(edits, None, cx); buffer.edit(edits, None, cx);
}); });
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.select_ranges(new_selections); s.select_ranges(new_selections);
});
editor.set_clip_at_line_ends(true, cx);
}); });
editor.set_clip_at_line_ends(true, cx);
}); });
}); }
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,14 +1,19 @@
use std::borrow::BorrowMut;
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range, sync::Arc};
use crate::command::command_interceptor;
use crate::normal::repeat::Replayer; use crate::normal::repeat::Replayer;
use crate::surrounds::SurroundsType; use crate::surrounds::SurroundsType;
use crate::{motion::Motion, object::Object}; use crate::{motion::Motion, object::Object};
use crate::{UseSystemClipboard, Vim, VimSettings};
use collections::HashMap; use collections::HashMap;
use editor::{Anchor, ClipboardSelection}; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
use gpui::{Action, ClipboardEntry, ClipboardItem, KeyContext}; use editor::{Anchor, ClipboardSelection, Editor};
use language::{CursorShape, Selection, TransactionId}; use gpui::{Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global};
use language::Point;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ui::SharedString; use settings::{Settings, SettingsStore};
use ui::{SharedString, ViewContext};
use workspace::searchable::Direction; use workspace::searchable::Direction;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
@ -75,32 +80,6 @@ pub enum Operator {
ToggleComments, 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)] #[derive(Default, Clone, Debug)]
pub enum RecordedSelection { pub enum RecordedSelection {
#[default] #[default]
@ -161,7 +140,7 @@ impl From<String> for Register {
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct WorkspaceState { pub struct VimGlobals {
pub last_find: Option<Motion>, pub last_find: Option<Motion>,
pub dot_recording: bool, pub dot_recording: bool,
@ -182,6 +161,232 @@ pub struct WorkspaceState {
pub registers: HashMap<char, Register>, pub registers: HashMap<char, Register>,
pub recordings: HashMap<char, Vec<ReplayableAction>>, 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)] #[derive(Debug)]
pub enum ReplayableAction { pub enum ReplayableAction {
@ -218,93 +423,6 @@ pub struct SearchState {
pub prior_mode: Mode, 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 { impl Operator {
pub fn id(&self) -> &'static str { pub fn id(&self) -> &'static str {
match self { match self {

View File

@ -5,10 +5,10 @@ use crate::{
Vim, Vim,
}; };
use editor::{movement, scroll::Autoscroll, Bias}; use editor::{movement, scroll::Autoscroll, Bias};
use gpui::WindowContext;
use language::BracketPair; use language::BracketPair;
use serde::Deserialize; use serde::Deserialize;
use std::sync::Arc; use std::sync::Arc;
use ui::ViewContext;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType { 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) { impl Vim {
Vim::update(cx, |vim, cx| { pub fn add_surrounds(
vim.stop_recording(); &mut self,
let count = vim.take_count(cx); text: Arc<str>,
let mode = vim.state().mode; target: SurroundsType,
vim.update_active_editor(cx, |_, editor, cx| { 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); let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, 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) { pub fn delete_surrounds(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { self.stop_recording(cx);
vim.stop_recording();
// only legitimate surrounds can be removed // only legitimate surrounds can be removed
let pair = match find_surround_pair(&all_support_surround_pair(), &text) { 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; let surround = pair.end != *text;
vim.update_active_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, 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); 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) { if let Some(will_replace_pair) = object_to_bracket_pair(target) {
Vim::update(cx, |vim, cx| { self.stop_recording(cx);
vim.stop_recording(); self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
@ -332,65 +333,67 @@ pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext)
}); });
}); });
}); });
}); }
} }
}
/// Checks if any of the current cursors are surrounded by a valid pair of brackets. /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
/// ///
/// This method supports multiple cursors and checks each cursor for a valid pair of brackets. /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
/// A pair of brackets is considered valid if it is well-formed and properly closed. /// A pair of brackets is considered valid if it is well-formed and properly closed.
/// ///
/// 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 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`. /// If no valid pair of brackets is found for any cursor, the method returns `false`.
pub fn check_and_move_to_valid_bracket_pair( pub fn check_and_move_to_valid_bracket_pair(
vim: &mut Vim, &mut self,
object: Object, object: Object,
cx: &mut WindowContext, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
let mut valid = false; let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) { 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.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx); let (display_map, selections) = editor.selections.all_adjusted_display(cx);
let mut anchors = Vec::new(); let mut anchors = Vec::new();
for selection in &selections { for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left); let start = selection.start.to_offset(&display_map, Bias::Left);
if let Some(range) = object.range(&display_map, selection.clone(), true) { if let Some(range) = object.range(&display_map, selection.clone(), true) {
// If the current parenthesis object is single-line, // If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not // then we need to filter whether it is the current line or not
if object.is_multiline() if object.is_multiline()
|| (!object.is_multiline() || (!object.is_multiline()
&& selection.start.row() == range.start.row() && selection.start.row() == range.start.row()
&& selection.end.row() == range.end.row()) && selection.end.row() == range.end.row())
{ {
valid = true; valid = true;
let mut chars_and_offset = display_map let mut chars_and_offset = display_map
.buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) .buffer_chars_at(
.peekable(); range.start.to_offset(&display_map, Bias::Left),
while let Some((ch, offset)) = chars_and_offset.next() { )
if ch.to_string() == pair.start { .peekable();
anchors.push(offset..offset); while let Some((ch, offset)) = chars_and_offset.next() {
break; if ch.to_string() == pair.start {
anchors.push(offset..offset);
break;
}
} }
} else {
anchors.push(start..start)
} }
} else { } else {
anchors.push(start..start) anchors.push(start..start)
} }
} else {
anchors.push(start..start)
} }
} editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(anchors);
s.select_ranges(anchors); });
editor.set_clip_at_line_ends(true, cx);
}); });
editor.set_clip_at_line_ends(true, cx);
}); });
}); }
return valid;
} }
return valid;
} }
fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {

View File

@ -17,7 +17,7 @@ use indoc::indoc;
use search::BufferSearchBar; use search::BufferSearchBar;
use workspace::WorkspaceSettings; use workspace::WorkspaceSettings;
use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator}; use crate::{insert::NormalBefore, motion, state::Mode};
#[gpui::test] #[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { 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ˇ"); cx.assert_editor_state("hjklˇ");
// Selections aren't changed if editor is blurred but vim-mode is still disabled. // 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.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.blur()); cx.update_editor(|_, cx| cx.blur());
cx.assert_editor_state("«hjklˇ»"); 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); 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] #[gpui::test]
async fn test_word_characters(cx: &mut gpui::TestAppContext) { async fn test_word_characters(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await; 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 util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, VimTestContext}; use super::{neovim_connection::NeovimConnection, VimTestContext};
use crate::{state::Mode, Vim}; use crate::state::{Mode, VimGlobals};
pub struct NeovimBackedTestContext { pub struct NeovimBackedTestContext {
cx: VimTestContext, cx: VimTestContext,
@ -263,8 +263,7 @@ impl NeovimBackedTestContext {
state: self.shared_state().await, state: self.shared_state().await,
neovim: self.neovim.read_register(register).await, neovim: self.neovim.read_register(register).await,
editor: self.update(|cx| { editor: self.update(|cx| {
Vim::read(cx) cx.global::<VimGlobals>()
.workspace_state
.registers .registers
.get(&register) .get(&register)
.cloned() .cloned()

View File

@ -1,7 +1,7 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use editor::test::editor_lsp_test_context::EditorLspTestContext; 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 search::{project_search::ProjectSearchBar, BufferSearchBar};
use crate::{state::Operator, *}; use crate::{state::Operator, *};
@ -12,7 +12,7 @@ pub struct VimTestContext {
impl VimTestContext { impl VimTestContext {
pub fn init(cx: &mut gpui::TestAppContext) { pub fn init(cx: &mut gpui::TestAppContext) {
if cx.has_global::<Vim>() { if cx.has_global::<VimGlobals>() {
return; return;
} }
cx.update(|cx| { cx.update(|cx| {
@ -119,23 +119,31 @@ impl VimTestContext {
} }
pub fn mode(&mut self) -> Mode { 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> { pub fn active_operator(&mut self) -> Option<Operator> {
self.cx self.update_editor(|editor, cx| {
.read(|cx| cx.global::<Vim>().state().operator_stack.last().cloned()) editor
.addon::<VimAddon>()
.unwrap()
.view
.read(cx)
.operator_stack
.last()
.cloned()
})
} }
pub fn set_state(&mut self, text: &str, mode: Mode) { pub fn set_state(&mut self, text: &str, mode: Mode) {
let window = self.window;
self.cx.set_state(text); self.cx.set_state(text);
self.update_window(window, |_, cx| { let vim = self.update_editor(|editor, _cx| editor.addon::<VimAddon>().cloned().unwrap());
Vim::update(cx, |vim, cx| {
self.update(|cx| {
vim.view.update(cx, |vim, cx| {
vim.switch_mode(mode, true, cx); vim.switch_mode(mode, true, cx);
}) });
}) });
.unwrap();
self.cx.cx.cx.run_until_parked(); 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, scroll::Autoscroll,
Bias, DisplayPoint, Editor, ToOffset, Bias, DisplayPoint, Editor, ToOffset,
}; };
use gpui::{actions, ViewContext, WindowContext}; use gpui::{actions, ViewContext};
use language::{Point, Selection, SelectionGoal}; use language::{Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use search::BufferSearchBar; use search::BufferSearchBar;
use util::ResultExt; use util::ResultExt;
use workspace::{searchable::Direction, Workspace}; use workspace::searchable::Direction;
use crate::{ use crate::{
motion::{start_of_line, Motion}, motion::{start_of_line, Motion},
normal::yank::{copy_selections_content, yank_selections_content},
normal::{mark::create_visual_marks, substitute::substitute},
object::Object, object::Object,
state::{Mode, Operator}, state::{Mode, Operator},
Vim, Vim,
@ -41,102 +39,87 @@ actions!(
] ]
); );
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| { Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| {
toggle_mode(Mode::Visual, cx) vim.toggle_mode(Mode::Visual, cx)
}); });
workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| { Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| {
toggle_mode(Mode::VisualLine, cx) vim.toggle_mode(Mode::VisualLine, cx)
}); });
workspace.register_action( Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| {
|_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| { vim.toggle_mode(Mode::VisualBlock, cx)
toggle_mode(Mode::VisualBlock, cx)
},
);
workspace.register_action(other_end);
workspace.register_action(|_, _: &VisualDelete, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
delete(vim, false, cx);
});
}); });
workspace.register_action(|_, _: &VisualDeleteLine, cx| { Vim::action(editor, cx, Vim::other_end);
Vim::update(cx, |vim, cx| { Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
vim.record_current_action(cx); vim.record_current_action(cx);
delete(vim, true, cx); vim.visual_delete(false, cx);
});
}); });
workspace.register_action(|_, _: &VisualYank, cx| { Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| {
Vim::update(cx, |vim, cx| { vim.record_current_action(cx);
yank(vim, 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);
});
Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| {
vim.select_match(Direction::Prev, cx);
}); });
workspace.register_action(select_next); Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| {
workspace.register_action(select_previous); let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
workspace.register_action(|workspace, _: &SelectNextMatch, cx| { return;
Vim::update(cx, |vim, cx| { };
select_match(workspace, vim, Direction::Next, cx); let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
}); return;
}); };
workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| { let ranges = start
Vim::update(cx, |vim, cx| { .into_iter()
select_match(workspace, vim, Direction::Prev, cx); .zip(end)
}); .zip(reversed)
}); .map(|((start, end), reversed)| (*start, *end, reversed))
.collect::<Vec<_>>();
workspace.register_action(|_, _: &RestoreVisualSelection, cx| { if vim.mode.is_visual() {
Vim::update(cx, |vim, cx| { vim.create_visual_marks(vim.mode, cx);
let Some((stored_mode, reversed)) = }
vim.update_state(|state| state.stored_visual_mode.take())
else {
return;
};
let Some((start, end)) = vim.state().marks.get("<").zip(vim.state().marks.get(">"))
else {
return;
};
let ranges = start
.into_iter()
.zip(end)
.zip(reversed)
.map(|((start, end), reversed)| (*start, *end, reversed))
.collect::<Vec<_>>();
if vim.state().mode.is_visual() { vim.update_editor(cx, |_, editor, cx| {
create_visual_marks(vim, vim.state().mode, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
} let map = s.display_map();
let ranges = ranges
vim.update_active_editor(cx, |_, editor, cx| { .into_iter()
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { .map(|(start, end, reversed)| {
let map = s.display_map(); let new_end = movement::saturating_right(&map, end.to_display_point(&map));
let ranges = ranges Selection {
.into_iter() id: s.new_selection_id(),
.map(|(start, end, reversed)| { start: start.to_offset(&map.buffer_snapshot),
let new_end = end: new_end.to_offset(&map, Bias::Left),
movement::saturating_right(&map, end.to_display_point(&map)); reversed,
Selection { goal: SelectionGoal::None,
id: s.new_selection_id(), }
start: start.to_offset(&map.buffer_snapshot), })
end: new_end.to_offset(&map, Bias::Left), .collect();
reversed, s.select(ranges);
goal: SelectionGoal::None, })
}
})
.collect();
s.select(ranges);
})
});
vim.switch_mode(stored_mode, true, cx)
}); });
vim.switch_mode(stored_mode, true, cx)
}); });
} }
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) { impl Vim {
Vim::update(cx, |vim, cx| { pub fn visual_motion(
vim.update_active_editor(cx, |vim, editor, cx| { &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); let text_layout_details = editor.text_layout_details(cx);
if vim.state().mode == Mode::VisualBlock if vim.mode == Mode::VisualBlock
&& !matches!( && !matches!(
motion, motion,
Motion::EndOfLine { 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 { .. }); 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) motion.move_point(map, point, goal, times, &text_layout_details)
}) })
} else { } 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. // ensure the current character is included in the selection.
if !selection.reversed { 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) movement::saturating_right(map, selection.end)
} else { } else {
movement::right(map, selection.end) movement::right(map, selection.end)
@ -206,127 +189,126 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
}); });
} }
}); });
}); }
}
pub fn visual_block_motion( pub fn visual_block_motion(
preserve_goal: bool, &mut self,
editor: &mut Editor, preserve_goal: bool,
cx: &mut ViewContext<Editor>, editor: &mut Editor,
mut move_selection: impl FnMut( cx: &mut ViewContext<Editor>,
&DisplaySnapshot, mut move_selection: impl FnMut(
DisplayPoint, &DisplaySnapshot,
SelectionGoal, DisplayPoint,
) -> Option<(DisplayPoint, SelectionGoal)>, SelectionGoal,
) { ) -> Option<(DisplayPoint, SelectionGoal)>,
let text_layout_details = editor.text_layout_details(cx); ) {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { let text_layout_details = editor.text_layout_details(cx);
let map = &s.display_map(); editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut head = s.newest_anchor().head().to_display_point(map); let map = &s.display_map();
let mut tail = s.oldest_anchor().tail().to_display_point(map); let mut head = s.newest_anchor().head().to_display_point(map);
let mut tail = s.oldest_anchor().tail().to_display_point(map);
let mut head_x = map.x_for_display_point(head, &text_layout_details); let mut head_x = map.x_for_display_point(head, &text_layout_details);
let mut tail_x = map.x_for_display_point(tail, &text_layout_details); let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
let (start, end) = match s.newest_anchor().goal { let (start, end) = match s.newest_anchor().goal {
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
_ => (tail_x.0, head_x.0), _ => (tail_x.0, head_x.0),
};
let mut goal = SelectionGoal::HorizontalRange { start, end };
let was_reversed = tail_x > head_x;
if !was_reversed && !preserve_goal {
head = movement::saturating_left(map, head);
}
let Some((new_head, _)) = move_selection(&map, head, goal) else {
return;
};
head = new_head;
head_x = map.x_for_display_point(head, &text_layout_details);
let is_reversed = tail_x > head_x;
if was_reversed && !is_reversed {
tail = movement::saturating_left(map, tail);
tail_x = map.x_for_display_point(tail, &text_layout_details);
} else if !was_reversed && is_reversed {
tail = movement::saturating_right(map, tail);
tail_x = map.x_for_display_point(tail, &text_layout_details);
}
if !is_reversed && !preserve_goal {
head = movement::saturating_right(map, head);
head_x = map.x_for_display_point(head, &text_layout_details);
}
let positions = if is_reversed {
head_x..tail_x
} else {
tail_x..head_x
};
if !preserve_goal {
goal = SelectionGoal::HorizontalRange {
start: positions.start.0,
end: positions.end.0,
}; };
} let mut goal = SelectionGoal::HorizontalRange { start, end };
let mut selections = Vec::new(); let was_reversed = tail_x > head_x;
let mut row = tail.row(); if !was_reversed && !preserve_goal {
head = movement::saturating_left(map, head);
}
loop { let Some((new_head, _)) = move_selection(&map, head, goal) else {
let laid_out_line = map.layout_row(row, &text_layout_details); return;
let start = DisplayPoint::new( };
row, head = new_head;
laid_out_line.closest_index_for_x(positions.start) as u32, head_x = map.x_for_display_point(head, &text_layout_details);
);
let mut end = let is_reversed = tail_x > head_x;
DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); if was_reversed && !is_reversed {
if end <= start { tail = movement::saturating_left(map, tail);
if start.column() == map.line_len(start.row()) { tail_x = map.x_for_display_point(tail, &text_layout_details);
end = start; } else if !was_reversed && is_reversed {
tail = movement::saturating_right(map, tail);
tail_x = map.x_for_display_point(tail, &text_layout_details);
}
if !is_reversed && !preserve_goal {
head = movement::saturating_right(map, head);
head_x = map.x_for_display_point(head, &text_layout_details);
}
let positions = if is_reversed {
head_x..tail_x
} else {
tail_x..head_x
};
if !preserve_goal {
goal = SelectionGoal::HorizontalRange {
start: positions.start.0,
end: positions.end.0,
};
}
let mut selections = Vec::new();
let mut row = tail.row();
loop {
let laid_out_line = map.layout_row(row, &text_layout_details);
let start = DisplayPoint::new(
row,
laid_out_line.closest_index_for_x(positions.start) as u32,
);
let mut end =
DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
if end <= start {
if start.column() == map.line_len(start.row()) {
end = start;
} else {
end = movement::saturating_right(map, start);
}
}
if positions.start <= laid_out_line.width {
let selection = Selection {
id: s.new_selection_id(),
start: start.to_point(map),
end: end.to_point(map),
reversed: is_reversed,
goal,
};
selections.push(selection);
}
if row == head.row() {
break;
}
if tail.row() > head.row() {
row.0 -= 1
} else { } else {
end = movement::saturating_right(map, start); row.0 += 1
} }
} }
if positions.start <= laid_out_line.width { s.select(selections);
let selection = Selection { })
id: s.new_selection_id(), }
start: start.to_point(map),
end: end.to_point(map),
reversed: is_reversed,
goal,
};
selections.push(selection); pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext<Vim>) {
} if let Some(Operator::Object { around }) = self.active_operator() {
if row == head.row() { self.pop_operator(cx);
break; let current_mode = self.mode;
}
if tail.row() > head.row() {
row.0 -= 1
} else {
row.0 += 1
}
}
s.select(selections);
})
}
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;
let target_mode = object.target_visual_mode(current_mode); let target_mode = object.target_visual_mode(current_mode);
if target_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| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let mut mut_selection = selection.clone(); let mut mut_selection = selection.clone();
@ -384,109 +366,103 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
}); });
}); });
} }
}); }
}
fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) { fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { if self.mode == mode {
if vim.state().mode == mode { self.switch_mode(Mode::Normal, false, cx);
vim.switch_mode(Mode::Normal, false, cx);
} else { } else {
vim.switch_mode(mode, false, cx); self.switch_mode(mode, false, cx);
} }
}) }
}
pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) { pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| { s.move_with(|_, selection| {
selection.reversed = !selection.reversed; selection.reversed = !selection.reversed;
}) })
}) })
}) });
}); }
}
pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) { pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext<Self>) {
vim.store_visual_marks(cx); self.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| { self.update_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default(); let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode; let line_mode = line_mode || editor.selections.line_mode;
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
if line_mode { if line_mode {
let mut position = selection.head(); let mut position = selection.head();
if !selection.reversed { if !selection.reversed {
position = movement::left(map, position); position = movement::left(map, position);
} }
original_columns.insert(selection.id, position.to_point(map).column); 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()) *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); selection.start = DisplayPoint::new(selection.start.row(), 0);
if selection.end.row() == map.max_point().row() { if selection.end.row() == map.max_point().row() {
selection.end = map.max_point() selection.end = map.max_point()
} else { } else {
*selection.end.row_mut() += 1; *selection.end.row_mut() += 1;
*selection.end.column_mut() = 0; *selection.end.column_mut() = 0;
}
} }
} }
} selection.goal = SelectionGoal::None;
selection.goal = SelectionGoal::None; });
}); });
}); vim.copy_selections_content(editor, line_mode, cx);
copy_selections_content(vim, editor, line_mode, cx); editor.insert("", cx);
editor.insert("", cx);
// Fixup cursor position after the deletion // Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx); editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map);
if let Some(column) = original_columns.get(&selection.id) {
cursor.column = *column
}
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
if vim.mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
})
});
self.switch_mode(Mode::Normal, true, 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;
vim.yank_selections_content(editor, line_mode, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map); if line_mode {
selection.start = start_of_line(map, false, selection.start);
if let Some(column) = original_columns.get(&selection.id) { };
cursor.column = *column selection.collapse_to(selection.start, SelectionGoal::None)
}
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()]) s.select_anchors(vec![s.first_anchor()])
} }
}); });
})
});
vim.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| {
let line_mode = editor.selections.line_mode;
yank_selections_content(vim, editor, line_mode, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if line_mode {
selection.start = start_of_line(map, false, selection.start);
};
selection.collapse_to(selection.start, SelectionGoal::None)
});
if vim.state().mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
}); });
}); self.switch_mode(Mode::Normal, true, cx);
vim.switch_mode(Mode::Normal, true, cx); }
}
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) { pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { self.stop_recording(cx);
vim.stop_recording(); self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(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)); 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>) { pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { let count = self
let count = .take_count(cx)
vim.take_count(cx) .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
for _ in 0..count { for _ in 0..count {
if editor if editor
@ -542,16 +516,14 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Works
break; break;
} }
} }
}) });
}); }
}
pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) { pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
Vim::update(cx, |vim, cx| { let count = self
let count = .take_count(cx)
vim.take_count(cx) .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); self.update_editor(cx, |_, editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
for _ in 0..count { for _ in 0..count {
if editor if editor
.select_previous(&Default::default(), cx) .select_previous(&Default::default(), cx)
@ -561,89 +533,91 @@ pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewConte
break; break;
} }
} }
}) });
}); }
}
pub fn select_match( pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
workspace: &mut Workspace, let count = self.take_count(cx).unwrap_or(1);
vim: &mut Vim, let Some(workspace) = self
direction: Direction, .editor
cx: &mut WindowContext, .upgrade()
) { .and_then(|editor| editor.read(cx).workspace())
let count = vim.take_count(cx).unwrap_or(1); else {
let pane = workspace.active_pane().clone(); return;
let vim_is_normal = vim.state().mode == Mode::Normal; };
let mut start_selection = 0usize; let pane = workspace.read(cx).active_pane().clone();
let mut end_selection = 0usize; 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); editor.set_collapse_matches(false);
}); });
if vim_is_normal { if vim_is_normal {
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| {
if !search_bar.has_active_match() || !search_bar.show(cx) {
return;
}
// without update_match_index there is a bug when the cursor is before the first match
search_bar.update_match_index(cx);
search_bar.select_match(direction.opposite(), 1, cx);
});
}
});
}
self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
start_selection = latest.start;
end_selection = latest.end;
});
let mut match_exists = false;
pane.update(cx, |pane, cx| { 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| { search_bar.update(cx, |search_bar, cx| {
if !search_bar.has_active_match() || !search_bar.show(cx) {
return;
}
// without update_match_index there is a bug when the cursor is before the first match
search_bar.update_match_index(cx); search_bar.update_match_index(cx);
search_bar.select_match(direction.opposite(), 1, cx); search_bar.select_match(direction, count, cx);
match_exists = search_bar.match_exists(cx);
}); });
} }
}); });
} if !match_exists {
vim.update_active_editor(cx, |_, editor, cx| { self.clear_operator(cx);
let latest = editor.selections.newest::<usize>(cx); self.stop_replaying(cx);
start_selection = latest.start; return;
end_selection = latest.end; }
}); self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
let mut match_exists = false; if vim_is_normal {
pane.update(cx, |pane, cx| { start_selection = latest.start;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() { end_selection = latest.end;
search_bar.update(cx, |search_bar, cx| { } else {
search_bar.update_match_index(cx); start_selection = start_selection.min(latest.start);
search_bar.select_match(direction, count, cx); end_selection = end_selection.max(latest.end);
match_exists = search_bar.match_exists(cx); }
if direction == Direction::Prev {
std::mem::swap(&mut start_selection, &mut end_selection);
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([start_selection..end_selection]);
}); });
} editor.set_collapse_matches(true);
});
if !match_exists {
vim.clear_operator(cx);
vim.stop_replaying(cx);
return;
}
vim.update_active_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
if vim_is_normal {
start_selection = latest.start;
end_selection = latest.end;
} else {
start_selection = start_selection.min(latest.start);
end_selection = end_selection.max(latest.end);
}
if direction == Direction::Prev {
std::mem::swap(&mut start_selection, &mut end_selection);
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([start_selection..end_selection]);
}); });
editor.set_collapse_matches(true);
});
match vim.maybe_pop_operator() { match self.maybe_pop_operator() {
Some(Operator::Change) => substitute(vim, None, false, cx), Some(Operator::Change) => self.substitute(None, false, cx),
Some(Operator::Delete) => { Some(Operator::Delete) => {
vim.stop_recording(); self.stop_recording(cx);
delete(vim, false, cx) self.visual_delete(false, cx)
}
Some(Operator::Yank) => self.visual_yank(cx),
_ => {} // Ignoring other operators
} }
Some(Operator::Yank) => yank(vim, cx),
_ => {} // Ignoring other operators
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use indoc::indoc; use indoc::indoc;