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:
parent
c4c07583c3
commit
36d51fe4a5
32 changed files with 3362 additions and 3585 deletions
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(®ister);
|
||||
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(®ister);
|
||||
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(®ister) else {
|
||||
let Some(actions) = globals.recordings.get(®ister) 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)]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue