vim: Improve lifecycle (#16477)

Closes #13579

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

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

Release Notes:

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,11 @@ use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use ui::WindowContext;
use util::ResultExt;
use workspace::{notifications::NotifyResultExt, SaveIntent, Workspace};
use workspace::{notifications::NotifyResultExt, SaveIntent};
use crate::{
motion::{EndOfDocument, Motion, StartOfDocument},
normal::{
move_cursor,
search::{FindCommand, ReplaceCommand, Replacement},
JoinLines,
},
@ -66,77 +65,89 @@ impl Clone for WithRange {
}
}
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &VisualCommand, cx| {
command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &VisualCommand, cx| {
let Some(workspace) = vim.workspace(cx) else {
return;
};
workspace.update(cx, |workspace, cx| {
command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
})
});
workspace.register_action(|workspace, _: &CountCommand, cx| {
let count = Vim::update(cx, |vim, cx| vim.take_count(cx)).unwrap_or(1);
command_palette::CommandPalette::toggle(
workspace,
&format!(".,.+{}", count.saturating_sub(1)),
cx,
);
});
workspace.register_action(|workspace: &mut Workspace, action: &GoToLine, cx| {
Vim::update(cx, |vim, cx| {
vim.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),
Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
let Some(workspace) = vim.workspace(cx) else {
return;
};
let count = vim.take_count(cx).unwrap_or(1);
workspace.update(cx, |workspace, cx| {
command_palette::CommandPalette::toggle(
workspace,
&format!(".,.+{}", count.saturating_sub(1)),
cx,
);
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 {
for _ in 0..action.range.as_count() {
cx.dispatch_action(action.action.boxed_clone())
}
} else {
Vim::update(cx, |vim, cx| {
let result = vim.update_active_editor(cx, |vim, editor, cx| {
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);
return;
}
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 {
Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
Position::Mark { name, offset } => {
let Some(mark) = vim
.state()
.marks
.get(&name.to_string())
.and_then(|vec| vec.last())
else {
let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
return Err(anyhow!("mark {} not set", name));
};
mark.to_point(&snapshot.buffer_snapshot)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
use crate::{
motion::{self, Motion},
normal::yank::copy_selections_content,
object::Object,
state::Mode,
Vim,
@ -11,98 +10,108 @@ use editor::{
scroll::Autoscroll,
Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::{char_kind, CharKind, Selection};
use ui::ViewContext;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
Motion::Left
| Motion::Right
| Motion::EndOfLine { .. }
| Motion::Backspace
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
impl Vim {
pub fn change_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
Motion::Left
| Motion::Right
| 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
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
}
}
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 {
vim.copy_selections_content(editor, false, cx);
editor.insert("", cx);
}
});
copy_selections_content(vim, editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
if motion_succeeded {
vim.switch_mode(Mode::Insert, false, cx)
} else {
vim.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);
if objects_found {
self.switch_mode(Mode::Insert, false, cx);
} else {
self.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 editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
scroll::Autoscroll,
Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::{Point, Selection};
use multi_buffer::MultiBufferRow;
use ui::ViewContext;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |vim, editor, cx| {
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_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);
impl Vim {
pub fn delete_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.stop_recording(cx);
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
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.
if let Motion::NextWordStart {
ignore_punctuation: _,
} = motion
{
if selection.is_empty()
&& map
.buffer_snapshot
.line_len(MultiBufferRow(selection.start.to_point(&map).row))
== 0
// Motion::NextWordStart on an empty line should delete it.
if let Motion::NextWordStart {
ignore_punctuation: _,
} = motion
{
selection.end = map
.buffer_snapshot
.clip_point(
Point::new(selection.start.to_point(&map).row + 1, 0),
Bias::Left,
)
.to_display_point(map)
}
}
});
});
copy_selections_content(vim, editor, motion.linewise(), 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 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);
if selection.is_empty()
&& map
.buffer_snapshot
.line_len(MultiBufferRow(selection.start.to_point(&map).row))
== 0
{
selection.end = map
.buffer_snapshot
.clip_point(
Point::new(selection.start.to_point(&map).row + 1, 0),
Bias::Left,
)
.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);
}
}
});
});
});
copy_selections_content(vim, editor, false, cx);
editor.insert("", cx);
vim.copy_selections_content(editor, motion.linewise(), 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)
// 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(&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>) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,181 +8,187 @@ use crate::{
};
use collections::HashMap;
use editor::{ClipboardSelection, Editor};
use gpui::WindowContext;
use gpui::ViewContext;
use language::Point;
use multi_buffer::MultiBufferRow;
use ui::ViewContext;
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
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;
fn copy_selections_content_internal(
vim: &mut Vim,
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();
impl Vim {
pub fn yank_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
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| {
state.marks.insert(
pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
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(),
selections
.iter()
.map(|s| buffer.anchor_before(s.start))
.collect(),
);
state.marks.insert(
self.marks.insert(
"]".to_string(),
selections
.iter()
.map(|s| buffer.anchor_after(s.end))
.collect(),
)
});
);
{
let mut is_first = true;
for selection in selections.iter() {
let mut start = selection.start;
let end = selection.end;
if is_first {
is_first = false;
} else {
text.push_str("\n");
{
let mut is_first = true;
for selection in selections.iter() {
let mut start = selection.start;
let end = selection.end;
if is_first {
is_first = false;
} else {
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());
vim.write_registers(
Register {
text: text.into(),
clipboard_selections: Some(clipboard_selections),
},
selected_register,
is_yank,
linewise,
cx,
);
let selected_register = self.selected_register.take();
Vim::update_globals(cx, |globals, cx| {
globals.write_registers(
Register {
text: text.into(),
clipboard_selections: Some(clipboard_selections),
},
selected_register,
is_yank,
linewise,
cx,
)
});
if !is_yank || vim.state().mode == Mode::Visual {
return;
}
if !is_yank || self.mode == Mode::Visual {
return;
}
editor.highlight_background::<HighlightOnYank>(
&ranges_to_highlight,
|colors| colors.editor_document_highlight_read_background,
cx,
);
cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
this.update(&mut cx, |editor, cx| {
editor.clear_background_highlights::<HighlightOnYank>(cx)
editor.highlight_background::<HighlightOnYank>(
&ranges_to_highlight,
|colors| colors.editor_document_highlight_read_background,
cx,
);
cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
this.update(&mut cx, |editor, cx| {
editor.clear_background_highlights::<HighlightOnYank>(cx)
})
.ok();
})
.ok();
})
.detach();
.detach();
}
}

View File

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

View File

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

View File

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

View File

@ -5,10 +5,10 @@ use crate::{
Vim,
};
use editor::{movement, scroll::Autoscroll, Bias};
use gpui::WindowContext;
use language::BracketPair;
use serde::Deserialize;
use std::sync::Arc;
use ui::ViewContext;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType {
@ -27,12 +27,17 @@ impl<'de> Deserialize<'de> for SurroundsType {
}
}
pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
let count = vim.take_count(cx);
let mode = vim.state().mode;
vim.update_active_editor(cx, |_, editor, cx| {
impl Vim {
pub fn add_surrounds(
&mut self,
text: Arc<str>,
target: SurroundsType,
cx: &mut ViewContext<Self>,
) {
self.stop_recording(cx);
let count = self.take_count(cx);
let mode = self.mode;
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -128,13 +133,11 @@ pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowConte
});
});
});
vim.switch_mode(Mode::Normal, false, cx);
});
}
self.switch_mode(Mode::Normal, false, cx);
}
pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
pub fn delete_surrounds(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
self.stop_recording(cx);
// only legitimate surrounds can be removed
let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
@ -147,7 +150,7 @@ pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
};
let surround = pair.end != *text;
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -224,14 +227,12 @@ pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
editor.set_clip_at_line_ends(true, cx);
});
});
});
}
}
pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
if let Some(will_replace_pair) = object_to_bracket_pair(target) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
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) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, 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.
///
/// 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.
///
/// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
/// If no valid pair of brackets is found for any cursor, the method returns `false`.
pub fn check_and_move_to_valid_bracket_pair(
vim: &mut Vim,
object: Object,
cx: &mut WindowContext,
) -> bool {
let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
let mut anchors = Vec::new();
/// 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.
/// 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 no valid pair of brackets is found for any cursor, the method returns `false`.
pub fn check_and_move_to_valid_bracket_pair(
&mut self,
object: Object,
cx: &mut ViewContext<Self>,
) -> bool {
let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) {
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
let mut anchors = Vec::new();
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
if let Some(range) = object.range(&display_map, selection.clone(), true) {
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
if object.is_multiline()
|| (!object.is_multiline()
&& selection.start.row() == range.start.row()
&& selection.end.row() == range.end.row())
{
valid = true;
let mut chars_and_offset = display_map
.buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
.peekable();
while let Some((ch, offset)) = chars_and_offset.next() {
if ch.to_string() == pair.start {
anchors.push(offset..offset);
break;
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
if let Some(range) = object.range(&display_map, selection.clone(), true) {
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
if object.is_multiline()
|| (!object.is_multiline()
&& selection.start.row() == range.start.row()
&& selection.end.row() == range.end.row())
{
valid = true;
let mut chars_and_offset = display_map
.buffer_chars_at(
range.start.to_offset(&display_map, Bias::Left),
)
.peekable();
while let Some((ch, offset)) = chars_and_offset.next() {
if ch.to_string() == pair.start {
anchors.push(offset..offset);
break;
}
}
} else {
anchors.push(start..start)
}
} else {
anchors.push(start..start)
}
} else {
anchors.push(start..start)
}
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(anchors);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
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> {

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,15 @@ use editor::{
scroll::Autoscroll,
Bias, DisplayPoint, Editor, ToOffset,
};
use gpui::{actions, ViewContext, WindowContext};
use gpui::{actions, ViewContext};
use language::{Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow;
use search::BufferSearchBar;
use util::ResultExt;
use workspace::{searchable::Direction, Workspace};
use workspace::searchable::Direction;
use crate::{
motion::{start_of_line, Motion},
normal::yank::{copy_selections_content, yank_selections_content},
normal::{mark::create_visual_marks, substitute::substitute},
object::Object,
state::{Mode, Operator},
Vim,
@ -41,102 +39,87 @@ actions!(
]
);
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::Visual, cx)
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| {
vim.toggle_mode(Mode::Visual, cx)
});
workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::VisualLine, cx)
Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| {
vim.toggle_mode(Mode::VisualLine, cx)
});
workspace.register_action(
|_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
toggle_mode(Mode::VisualBlock, cx)
},
);
workspace.register_action(other_end);
workspace.register_action(|_, _: &VisualDelete, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
delete(vim, false, cx);
});
Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| {
vim.toggle_mode(Mode::VisualBlock, cx)
});
workspace.register_action(|_, _: &VisualDeleteLine, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
delete(vim, true, cx);
});
Vim::action(editor, cx, Vim::other_end);
Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
vim.record_current_action(cx);
vim.visual_delete(false, cx);
});
workspace.register_action(|_, _: &VisualYank, cx| {
Vim::update(cx, |vim, cx| {
yank(vim, cx);
});
Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| {
vim.record_current_action(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);
workspace.register_action(select_previous);
workspace.register_action(|workspace, _: &SelectNextMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Next, cx);
});
});
workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Prev, cx);
});
});
Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| {
let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
return;
};
let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
return;
};
let ranges = start
.into_iter()
.zip(end)
.zip(reversed)
.map(|((start, end), reversed)| (*start, *end, reversed))
.collect::<Vec<_>>();
workspace.register_action(|_, _: &RestoreVisualSelection, cx| {
Vim::update(cx, |vim, cx| {
let Some((stored_mode, reversed)) =
vim.update_state(|state| state.stored_visual_mode.take())
else {
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.mode.is_visual() {
vim.create_visual_marks(vim.mode, cx);
}
if vim.state().mode.is_visual() {
create_visual_marks(vim, vim.state().mode, cx);
}
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = s.display_map();
let ranges = ranges
.into_iter()
.map(|(start, end, reversed)| {
let new_end =
movement::saturating_right(&map, end.to_display_point(&map));
Selection {
id: s.new_selection_id(),
start: start.to_offset(&map.buffer_snapshot),
end: new_end.to_offset(&map, Bias::Left),
reversed,
goal: SelectionGoal::None,
}
})
.collect();
s.select(ranges);
})
});
vim.switch_mode(stored_mode, true, cx)
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = s.display_map();
let ranges = ranges
.into_iter()
.map(|(start, end, reversed)| {
let new_end = movement::saturating_right(&map, end.to_display_point(&map));
Selection {
id: s.new_selection_id(),
start: start.to_offset(&map.buffer_snapshot),
end: new_end.to_offset(&map, Bias::Left),
reversed,
goal: SelectionGoal::None,
}
})
.collect();
s.select(ranges);
})
});
vim.switch_mode(stored_mode, true, cx)
});
}
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
impl Vim {
pub fn visual_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
if vim.state().mode == Mode::VisualBlock
if vim.mode == Mode::VisualBlock
&& !matches!(
motion,
Motion::EndOfLine {
@ -145,7 +128,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
)
{
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
vim.visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
motion.move_point(map, point, goal, times, &text_layout_details)
})
} else {
@ -183,7 +166,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
// ensure the current character is included in the selection.
if !selection.reversed {
let next_point = if vim.state().mode == Mode::VisualBlock {
let next_point = if vim.mode == Mode::VisualBlock {
movement::saturating_right(map, selection.end)
} else {
movement::right(map, selection.end)
@ -206,127 +189,126 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
});
}
});
});
}
}
pub fn visual_block_motion(
preserve_goal: bool,
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
mut move_selection: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = &s.display_map();
let mut head = s.newest_anchor().head().to_display_point(map);
let mut tail = s.oldest_anchor().tail().to_display_point(map);
pub fn visual_block_motion(
&mut self,
preserve_goal: bool,
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
mut move_selection: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = &s.display_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 tail_x = map.x_for_display_point(tail, &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 (start, end) = match s.newest_anchor().goal {
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
_ => (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 (start, end) = match s.newest_anchor().goal {
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
_ => (tail_x.0, head_x.0),
};
}
let mut goal = SelectionGoal::HorizontalRange { start, end };
let mut selections = Vec::new();
let mut row = tail.row();
let was_reversed = tail_x > head_x;
if !was_reversed && !preserve_goal {
head = movement::saturating_left(map, head);
}
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;
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 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 {
end = movement::saturating_right(map, start);
row.0 += 1
}
}
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,
};
s.select(selections);
})
}
selections.push(selection);
}
if row == head.row() {
break;
}
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;
pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext<Vim>) {
if let Some(Operator::Object { around }) = self.active_operator() {
self.pop_operator(cx);
let current_mode = self.mode;
let target_mode = object.target_visual_mode(current_mode);
if target_mode != current_mode {
vim.switch_mode(target_mode, true, cx);
self.switch_mode(target_mode, true, cx);
}
vim.update_active_editor(cx, |_, editor, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut mut_selection = selection.clone();
@ -384,109 +366,103 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
});
});
}
});
}
}
fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
if vim.state().mode == mode {
vim.switch_mode(Mode::Normal, false, cx);
fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
if self.mode == mode {
self.switch_mode(Mode::Normal, false, cx);
} else {
vim.switch_mode(mode, false, cx);
self.switch_mode(mode, false, cx);
}
})
}
}
pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext<Self>) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
})
})
})
});
}
});
}
pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
vim.store_visual_marks(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode;
pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext<Self>) {
self.store_visual_marks(cx);
self.update_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode;
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if line_mode {
let mut position = selection.head();
if !selection.reversed {
position = movement::left(map, position);
}
original_columns.insert(selection.id, position.to_point(map).column);
if vim.state().mode == Mode::VisualBlock {
*selection.end.column_mut() = map.line_len(selection.end.row())
} else if vim.state().mode != Mode::VisualLine {
selection.start = DisplayPoint::new(selection.start.row(), 0);
if selection.end.row() == map.max_point().row() {
selection.end = map.max_point()
} else {
*selection.end.row_mut() += 1;
*selection.end.column_mut() = 0;
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if line_mode {
let mut position = selection.head();
if !selection.reversed {
position = movement::left(map, position);
}
original_columns.insert(selection.id, position.to_point(map).column);
if vim.mode == Mode::VisualBlock {
*selection.end.column_mut() = map.line_len(selection.end.row())
} else if vim.mode != Mode::VisualLine {
selection.start = DisplayPoint::new(selection.start.row(), 0);
if selection.end.row() == map.max_point().row() {
selection.end = map.max_point()
} else {
*selection.end.row_mut() += 1;
*selection.end.column_mut() = 0;
}
}
}
}
selection.goal = SelectionGoal::None;
selection.goal = SelectionGoal::None;
});
});
});
copy_selections_content(vim, editor, line_mode, cx);
editor.insert("", cx);
vim.copy_selections_content(editor, line_mode, 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| {
// 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().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| {
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 line_mode {
selection.start = start_of_line(map, false, selection.start);
};
selection.collapse_to(selection.start, SelectionGoal::None)
});
if vim.state().mode == Mode::VisualBlock {
if vim.mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
})
});
vim.switch_mode(Mode::Normal, true, cx);
}
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()])
}
});
});
vim.switch_mode(Mode::Normal, true, cx);
}
self.switch_mode(Mode::Normal, true, cx);
}
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
@ -522,16 +498,14 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
});
});
vim.switch_mode(Mode::Normal, false, cx);
});
}
self.switch_mode(Mode::Normal, false, cx);
}
pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let count = self
.take_count(cx)
.unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
for _ in 0..count {
if editor
@ -542,16 +516,14 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Works
break;
}
}
})
});
}
});
}
pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
let count = self
.take_count(cx)
.unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(cx, |_, editor, cx| {
for _ in 0..count {
if editor
.select_previous(&Default::default(), cx)
@ -561,89 +533,91 @@ pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewConte
break;
}
}
})
});
}
});
}
pub fn select_match(
workspace: &mut Workspace,
vim: &mut Vim,
direction: Direction,
cx: &mut WindowContext,
) {
let count = vim.take_count(cx).unwrap_or(1);
let pane = workspace.active_pane().clone();
let vim_is_normal = vim.state().mode == Mode::Normal;
let mut start_selection = 0usize;
let mut end_selection = 0usize;
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let count = self.take_count(cx).unwrap_or(1);
let Some(workspace) = self
.editor
.upgrade()
.and_then(|editor| editor.read(cx).workspace())
else {
return;
};
let pane = workspace.read(cx).active_pane().clone();
let vim_is_normal = self.mode == Mode::Normal;
let mut start_selection = 0usize;
let mut end_selection = 0usize;
vim.update_active_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false);
});
if vim_is_normal {
self.update_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false);
});
if vim_is_normal {
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
{
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| {
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);
search_bar.select_match(direction, count, cx);
match_exists = search_bar.match_exists(cx);
});
}
});
}
vim.update_active_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| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
search_bar.update_match_index(cx);
search_bar.select_match(direction, count, cx);
match_exists = search_bar.match_exists(cx);
if !match_exists {
self.clear_operator(cx);
self.stop_replaying(cx);
return;
}
self.update_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]);
});
}
});
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);
});
editor.set_collapse_matches(true);
});
match vim.maybe_pop_operator() {
Some(Operator::Change) => substitute(vim, None, false, cx),
Some(Operator::Delete) => {
vim.stop_recording();
delete(vim, false, cx)
match self.maybe_pop_operator() {
Some(Operator::Change) => self.substitute(None, false, cx),
Some(Operator::Delete) => {
self.stop_recording(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)]
mod test {
use indoc::indoc;