There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
1498 lines
53 KiB
Rust
1498 lines
53 KiB
Rust
use std::sync::Arc;
|
|
|
|
use collections::HashMap;
|
|
use editor::{
|
|
display_map::{DisplayRow, DisplaySnapshot, ToDisplayPoint},
|
|
movement,
|
|
scroll::Autoscroll,
|
|
Bias, DisplayPoint, Editor, ToOffset,
|
|
};
|
|
use gpui::{actions, Context, Window};
|
|
use language::{Point, Selection, SelectionGoal};
|
|
use multi_buffer::MultiBufferRow;
|
|
use search::BufferSearchBar;
|
|
use util::ResultExt;
|
|
use workspace::searchable::Direction;
|
|
|
|
use crate::{
|
|
motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
|
|
object::Object,
|
|
state::{Mode, Operator},
|
|
Vim,
|
|
};
|
|
|
|
actions!(
|
|
vim,
|
|
[
|
|
ToggleVisual,
|
|
ToggleVisualLine,
|
|
ToggleVisualBlock,
|
|
VisualDelete,
|
|
VisualDeleteLine,
|
|
VisualYank,
|
|
VisualYankLine,
|
|
OtherEnd,
|
|
SelectNext,
|
|
SelectPrevious,
|
|
SelectNextMatch,
|
|
SelectPreviousMatch,
|
|
SelectSmallerSyntaxNode,
|
|
SelectLargerSyntaxNode,
|
|
RestoreVisualSelection,
|
|
VisualInsertEndOfLine,
|
|
VisualInsertFirstNonWhiteSpace,
|
|
]
|
|
);
|
|
|
|
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|
Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| {
|
|
vim.toggle_mode(Mode::Visual, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| {
|
|
vim.toggle_mode(Mode::VisualLine, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| {
|
|
vim.toggle_mode(Mode::VisualBlock, window, cx)
|
|
});
|
|
Vim::action(editor, cx, Vim::other_end);
|
|
Vim::action(editor, cx, Vim::visual_insert_end_of_line);
|
|
Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
|
|
Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
|
|
vim.record_current_action(cx);
|
|
vim.visual_delete(false, window, cx);
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| {
|
|
vim.record_current_action(cx);
|
|
vim.visual_delete(true, window, cx);
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| {
|
|
vim.visual_yank(false, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| {
|
|
vim.visual_yank(true, window, cx)
|
|
});
|
|
|
|
Vim::action(editor, cx, Vim::select_next);
|
|
Vim::action(editor, cx, Vim::select_previous);
|
|
Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| {
|
|
vim.select_match(Direction::Next, window, cx);
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| {
|
|
vim.select_match(Direction::Prev, window, cx);
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| {
|
|
let count = Vim::take_count(cx).unwrap_or(1);
|
|
for _ in 0..count {
|
|
vim.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.select_larger_syntax_node(&Default::default(), window, cx);
|
|
});
|
|
}
|
|
});
|
|
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, _: &SelectSmallerSyntaxNode, window, cx| {
|
|
let count = Vim::take_count(cx).unwrap_or(1);
|
|
for _ in 0..count {
|
|
vim.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.select_smaller_syntax_node(&Default::default(), window, cx);
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| {
|
|
let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
|
|
return;
|
|
};
|
|
let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
|
|
return;
|
|
};
|
|
let ranges = start
|
|
.iter()
|
|
.zip(end)
|
|
.zip(reversed)
|
|
.map(|((start, end), reversed)| (*start, *end, reversed))
|
|
.collect::<Vec<_>>();
|
|
|
|
if vim.mode.is_visual() {
|
|
vim.create_visual_marks(vim.mode, window, cx);
|
|
}
|
|
|
|
vim.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
let map = s.display_map();
|
|
let ranges = ranges
|
|
.into_iter()
|
|
.map(|(start, end, reversed)| {
|
|
let new_end = movement::saturating_right(&map, end.to_display_point(&map));
|
|
Selection {
|
|
id: s.new_selection_id(),
|
|
start: start.to_offset(&map.buffer_snapshot),
|
|
end: new_end.to_offset(&map, Bias::Left),
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
})
|
|
.collect();
|
|
s.select(ranges);
|
|
})
|
|
});
|
|
vim.switch_mode(stored_mode, true, window, cx)
|
|
});
|
|
}
|
|
|
|
impl Vim {
|
|
pub fn visual_motion(
|
|
&mut self,
|
|
motion: Motion,
|
|
times: Option<usize>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.update_editor(window, cx, |vim, editor, window, cx| {
|
|
let text_layout_details = editor.text_layout_details(window);
|
|
if vim.mode == Mode::VisualBlock
|
|
&& !matches!(
|
|
motion,
|
|
Motion::EndOfLine {
|
|
display_lines: false
|
|
}
|
|
)
|
|
{
|
|
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
|
|
vim.visual_block_motion(is_up_or_down, editor, window, cx, |map, point, goal| {
|
|
motion.move_point(map, point, goal, times, &text_layout_details)
|
|
})
|
|
} else {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let was_reversed = selection.reversed;
|
|
let mut current_head = selection.head();
|
|
|
|
// our motions assume the current character is after the cursor,
|
|
// but in (forward) visual mode the current character is just
|
|
// before the end of the selection.
|
|
|
|
// If the file ends with a newline (which is common) we don't do this.
|
|
// so that if you go to the end of such a file you can use "up" to go
|
|
// to the previous line and have it work somewhat as expected.
|
|
#[allow(clippy::nonminimal_bool)]
|
|
if !selection.reversed
|
|
&& !selection.is_empty()
|
|
&& !(selection.end.column() == 0 && selection.end == map.max_point())
|
|
{
|
|
current_head = movement::left(map, selection.end)
|
|
}
|
|
|
|
let Some((new_head, goal)) = motion.move_point(
|
|
map,
|
|
current_head,
|
|
selection.goal,
|
|
times,
|
|
&text_layout_details,
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
selection.set_head(new_head, goal);
|
|
|
|
// ensure the current character is included in the selection.
|
|
if !selection.reversed {
|
|
let next_point = if vim.mode == Mode::VisualBlock {
|
|
movement::saturating_right(map, selection.end)
|
|
} else {
|
|
movement::right(map, selection.end)
|
|
};
|
|
|
|
if !(next_point.column() == 0 && next_point == map.max_point()) {
|
|
selection.end = next_point;
|
|
}
|
|
}
|
|
|
|
// vim always ensures the anchor character stays selected.
|
|
// if our selection has reversed, we need to move the opposite end
|
|
// to ensure the anchor is still selected.
|
|
if was_reversed && !selection.reversed {
|
|
selection.start = movement::left(map, selection.start);
|
|
} else if !was_reversed && selection.reversed {
|
|
selection.end = movement::right(map, selection.end);
|
|
}
|
|
})
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn visual_block_motion(
|
|
&mut self,
|
|
preserve_goal: bool,
|
|
editor: &mut Editor,
|
|
window: &mut Window,
|
|
cx: &mut Context<Editor>,
|
|
mut move_selection: impl FnMut(
|
|
&DisplaySnapshot,
|
|
DisplayPoint,
|
|
SelectionGoal,
|
|
) -> Option<(DisplayPoint, SelectionGoal)>,
|
|
) {
|
|
let text_layout_details = editor.text_layout_details(window);
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
let map = &s.display_map();
|
|
let mut head = s.newest_anchor().head().to_display_point(map);
|
|
let mut tail = s.oldest_anchor().tail().to_display_point(map);
|
|
|
|
let mut head_x = map.x_for_display_point(head, &text_layout_details);
|
|
let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
|
|
|
|
let (start, end) = match s.newest_anchor().goal {
|
|
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
|
|
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
|
|
_ => (tail_x.0, head_x.0),
|
|
};
|
|
let mut goal = SelectionGoal::HorizontalRange { start, end };
|
|
|
|
let was_reversed = tail_x > head_x;
|
|
if !was_reversed && !preserve_goal {
|
|
head = movement::saturating_left(map, head);
|
|
}
|
|
|
|
let Some((new_head, _)) = move_selection(map, head, goal) else {
|
|
return;
|
|
};
|
|
head = new_head;
|
|
head_x = map.x_for_display_point(head, &text_layout_details);
|
|
|
|
let is_reversed = tail_x > head_x;
|
|
if was_reversed && !is_reversed {
|
|
tail = movement::saturating_left(map, tail);
|
|
tail_x = map.x_for_display_point(tail, &text_layout_details);
|
|
} else if !was_reversed && is_reversed {
|
|
tail = movement::saturating_right(map, tail);
|
|
tail_x = map.x_for_display_point(tail, &text_layout_details);
|
|
}
|
|
if !is_reversed && !preserve_goal {
|
|
head = movement::saturating_right(map, head);
|
|
head_x = map.x_for_display_point(head, &text_layout_details);
|
|
}
|
|
|
|
let positions = if is_reversed {
|
|
head_x..tail_x
|
|
} else {
|
|
tail_x..head_x
|
|
};
|
|
|
|
if !preserve_goal {
|
|
goal = SelectionGoal::HorizontalRange {
|
|
start: positions.start.0,
|
|
end: positions.end.0,
|
|
};
|
|
}
|
|
|
|
let mut selections = Vec::new();
|
|
let mut row = tail.row();
|
|
|
|
loop {
|
|
let laid_out_line = map.layout_row(row, &text_layout_details);
|
|
let start = DisplayPoint::new(
|
|
row,
|
|
laid_out_line.closest_index_for_x(positions.start) as u32,
|
|
);
|
|
let mut end =
|
|
DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
|
|
if end <= start {
|
|
if start.column() == map.line_len(start.row()) {
|
|
end = start;
|
|
} else {
|
|
end = movement::saturating_right(map, start);
|
|
}
|
|
}
|
|
|
|
if positions.start <= laid_out_line.width {
|
|
let selection = Selection {
|
|
id: s.new_selection_id(),
|
|
start: start.to_point(map),
|
|
end: end.to_point(map),
|
|
reversed: is_reversed,
|
|
goal,
|
|
};
|
|
|
|
selections.push(selection);
|
|
}
|
|
if row == head.row() {
|
|
break;
|
|
}
|
|
if tail.row() > head.row() {
|
|
row.0 -= 1
|
|
} else {
|
|
row.0 += 1
|
|
}
|
|
}
|
|
|
|
s.select(selections);
|
|
})
|
|
}
|
|
|
|
pub fn visual_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Vim>) {
|
|
if let Some(Operator::Object { around }) = self.active_operator() {
|
|
self.pop_operator(window, cx);
|
|
let current_mode = self.mode;
|
|
let target_mode = object.target_visual_mode(current_mode, around);
|
|
if target_mode != current_mode {
|
|
self.switch_mode(target_mode, true, window, cx);
|
|
}
|
|
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let mut mut_selection = selection.clone();
|
|
|
|
// all our motions assume that the current character is
|
|
// after the cursor; however in the case of a visual selection
|
|
// the current character is before the cursor.
|
|
// But this will affect the judgment of the html tag
|
|
// so the html tag needs to skip this logic.
|
|
if !selection.reversed && object != Object::Tag {
|
|
mut_selection.set_head(
|
|
movement::left(map, mut_selection.head()),
|
|
mut_selection.goal,
|
|
);
|
|
}
|
|
|
|
if let Some(range) = object.range(map, mut_selection, around) {
|
|
if !range.is_empty() {
|
|
let expand_both_ways = object.always_expands_both_ways()
|
|
|| selection.is_empty()
|
|
|| movement::right(map, selection.start) == selection.end;
|
|
|
|
if expand_both_ways {
|
|
selection.start = range.start;
|
|
selection.end = range.end;
|
|
} else if selection.reversed {
|
|
selection.start = range.start;
|
|
} else {
|
|
selection.end = range.end;
|
|
}
|
|
}
|
|
|
|
// In the visual selection result of a paragraph object, the cursor is
|
|
// placed at the start of the last line. And in the visual mode, the
|
|
// selection end is located after the end character. So, adjustment of
|
|
// selection end is needed.
|
|
//
|
|
// We don't do this adjustment for a one-line blank paragraph since the
|
|
// trailing newline is included in its selection from the beginning.
|
|
if object == Object::Paragraph && range.start != range.end {
|
|
let row_of_selection_end_line = selection.end.to_point(map).row;
|
|
let new_selection_end = if map
|
|
.buffer_snapshot
|
|
.line_len(MultiBufferRow(row_of_selection_end_line))
|
|
== 0
|
|
{
|
|
Point::new(row_of_selection_end_line + 1, 0)
|
|
} else {
|
|
Point::new(row_of_selection_end_line, 1)
|
|
};
|
|
selection.end = new_selection_end.to_display_point(map);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
fn visual_insert_end_of_line(
|
|
&mut self,
|
|
_: &VisualInsertEndOfLine,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.split_selection_into_lines(&Default::default(), window, cx);
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_cursors_with(|map, cursor, _| {
|
|
(next_line_end(map, cursor, 1), SelectionGoal::None)
|
|
});
|
|
});
|
|
});
|
|
|
|
self.switch_mode(Mode::Insert, false, window, cx);
|
|
}
|
|
|
|
fn visual_insert_first_non_white_space(
|
|
&mut self,
|
|
_: &VisualInsertFirstNonWhiteSpace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.split_selection_into_lines(&Default::default(), window, cx);
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_cursors_with(|map, cursor, _| {
|
|
(
|
|
first_non_whitespace(map, false, cursor),
|
|
SelectionGoal::None,
|
|
)
|
|
});
|
|
});
|
|
});
|
|
|
|
self.switch_mode(Mode::Insert, false, window, cx);
|
|
}
|
|
|
|
fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.mode == mode {
|
|
self.switch_mode(Mode::Normal, false, window, cx);
|
|
} else {
|
|
self.switch_mode(mode, false, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_with(|_, selection| {
|
|
selection.reversed = !selection.reversed;
|
|
})
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.store_visual_marks(window, cx);
|
|
self.update_editor(window, cx, |vim, editor, window, cx| {
|
|
let mut original_columns: HashMap<_, _> = Default::default();
|
|
let line_mode = line_mode || editor.selections.line_mode;
|
|
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
if line_mode {
|
|
let mut position = selection.head();
|
|
if !selection.reversed {
|
|
position = movement::left(map, position);
|
|
}
|
|
original_columns.insert(selection.id, position.to_point(map).column);
|
|
if vim.mode == Mode::VisualBlock {
|
|
*selection.end.column_mut() = map.line_len(selection.end.row())
|
|
} else if vim.mode != Mode::VisualLine {
|
|
selection.start = DisplayPoint::new(selection.start.row(), 0);
|
|
selection.end =
|
|
map.next_line_boundary(selection.end.to_point(map)).1;
|
|
if selection.end.row() == map.max_point().row() {
|
|
selection.end = map.max_point();
|
|
if selection.start == selection.end {
|
|
let prev_row =
|
|
DisplayRow(selection.start.row().0.saturating_sub(1));
|
|
selection.start =
|
|
DisplayPoint::new(prev_row, map.line_len(prev_row));
|
|
}
|
|
} else {
|
|
*selection.end.row_mut() += 1;
|
|
*selection.end.column_mut() = 0;
|
|
}
|
|
}
|
|
}
|
|
selection.goal = SelectionGoal::None;
|
|
});
|
|
});
|
|
vim.copy_selections_content(editor, line_mode, cx);
|
|
editor.insert("", window, cx);
|
|
|
|
// Fixup cursor position after the deletion
|
|
editor.set_clip_at_line_ends(true, cx);
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let mut cursor = selection.head().to_point(map);
|
|
|
|
if let Some(column) = original_columns.get(&selection.id) {
|
|
cursor.column = *column
|
|
}
|
|
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
|
|
selection.collapse_to(cursor, selection.goal)
|
|
});
|
|
if vim.mode == Mode::VisualBlock {
|
|
s.select_anchors(vec![s.first_anchor()])
|
|
}
|
|
});
|
|
})
|
|
});
|
|
self.switch_mode(Mode::Normal, true, window, cx);
|
|
}
|
|
|
|
pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.store_visual_marks(window, cx);
|
|
self.update_editor(window, cx, |vim, editor, window, cx| {
|
|
let line_mode = line_mode || editor.selections.line_mode;
|
|
editor.selections.line_mode = line_mode;
|
|
vim.yank_selections_content(editor, line_mode, cx);
|
|
editor.change_selections(None, window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
if line_mode {
|
|
selection.start = start_of_line(map, false, selection.start);
|
|
};
|
|
selection.collapse_to(selection.start, SelectionGoal::None)
|
|
});
|
|
if vim.mode == Mode::VisualBlock {
|
|
s.select_anchors(vec![s.first_anchor()])
|
|
}
|
|
});
|
|
});
|
|
self.switch_mode(Mode::Normal, true, window, cx);
|
|
}
|
|
|
|
pub(crate) fn visual_replace(
|
|
&mut self,
|
|
text: Arc<str>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.stop_recording(cx);
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
|
|
|
|
// Selections are biased right at the start. So we need to store
|
|
// anchors that are biased left so that we can restore the selections
|
|
// after the change
|
|
let stable_anchors = editor
|
|
.selections
|
|
.disjoint_anchors()
|
|
.iter()
|
|
.map(|selection| {
|
|
let start = selection.start.bias_left(&display_map.buffer_snapshot);
|
|
start..start
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut edits = Vec::new();
|
|
for selection in selections.iter() {
|
|
let selection = selection.clone();
|
|
for row_range in
|
|
movement::split_display_range_by_lines(&display_map, selection.range())
|
|
{
|
|
let range = row_range.start.to_offset(&display_map, Bias::Right)
|
|
..row_range.end.to_offset(&display_map, Bias::Right);
|
|
let text = text.repeat(range.len());
|
|
edits.push((range, text));
|
|
}
|
|
}
|
|
|
|
editor.edit(edits, cx);
|
|
editor.change_selections(None, window, cx, |s| s.select_ranges(stable_anchors));
|
|
});
|
|
});
|
|
self.switch_mode(Mode::Normal, false, window, cx);
|
|
}
|
|
|
|
pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
|
let count =
|
|
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
for _ in 0..count {
|
|
if editor
|
|
.select_next(&Default::default(), window, cx)
|
|
.log_err()
|
|
.is_none()
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn select_previous(
|
|
&mut self,
|
|
_: &SelectPrevious,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let count =
|
|
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
for _ in 0..count {
|
|
if editor
|
|
.select_previous(&Default::default(), window, cx)
|
|
.log_err()
|
|
.is_none()
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn select_match(
|
|
&mut self,
|
|
direction: Direction,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let count = Vim::take_count(cx).unwrap_or(1);
|
|
let Some(pane) = self.pane(window, cx) else {
|
|
return;
|
|
};
|
|
let vim_is_normal = self.mode == Mode::Normal;
|
|
let mut start_selection = 0usize;
|
|
let mut end_selection = 0usize;
|
|
|
|
self.update_editor(window, cx, |_, editor, _, _| {
|
|
editor.set_collapse_matches(false);
|
|
});
|
|
if vim_is_normal {
|
|
pane.update(cx, |pane, cx| {
|
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
|
|
{
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
if !search_bar.has_active_match() || !search_bar.show(window, cx) {
|
|
return;
|
|
}
|
|
// without update_match_index there is a bug when the cursor is before the first match
|
|
search_bar.update_match_index(window, cx);
|
|
search_bar.select_match(direction.opposite(), 1, window, cx);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
self.update_editor(window, cx, |_, editor, _, cx| {
|
|
let latest = editor.selections.newest::<usize>(cx);
|
|
start_selection = latest.start;
|
|
end_selection = latest.end;
|
|
});
|
|
|
|
let mut match_exists = false;
|
|
pane.update(cx, |pane, cx| {
|
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
search_bar.update_match_index(window, cx);
|
|
search_bar.select_match(direction, count, window, cx);
|
|
match_exists = search_bar.match_exists(window, cx);
|
|
});
|
|
}
|
|
});
|
|
if !match_exists {
|
|
self.clear_operator(window, cx);
|
|
self.stop_replaying(cx);
|
|
return;
|
|
}
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
let latest = editor.selections.newest::<usize>(cx);
|
|
if vim_is_normal {
|
|
start_selection = latest.start;
|
|
end_selection = latest.end;
|
|
} else {
|
|
start_selection = start_selection.min(latest.start);
|
|
end_selection = end_selection.max(latest.end);
|
|
}
|
|
if direction == Direction::Prev {
|
|
std::mem::swap(&mut start_selection, &mut end_selection);
|
|
}
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.select_ranges([start_selection..end_selection]);
|
|
});
|
|
editor.set_collapse_matches(true);
|
|
});
|
|
|
|
match self.maybe_pop_operator() {
|
|
Some(Operator::Change) => self.substitute(None, false, window, cx),
|
|
Some(Operator::Delete) => {
|
|
self.stop_recording(cx);
|
|
self.visual_delete(false, window, cx)
|
|
}
|
|
Some(Operator::Yank) => self.visual_yank(false, window, cx),
|
|
_ => {} // Ignoring other operators
|
|
}
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
mod test {
|
|
use indoc::indoc;
|
|
use workspace::item::Item;
|
|
|
|
use crate::{
|
|
state::Mode,
|
|
test::{NeovimBackedTestContext, VimTestContext},
|
|
};
|
|
|
|
#[gpui::test]
|
|
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"
|
|
})
|
|
.await;
|
|
let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
|
|
|
|
// entering visual mode should select the character
|
|
// under cursor
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! { "The «qˇ»uick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
|
|
|
|
// forwards motions should extend the selection
|
|
cx.simulate_shared_keystrokes("w j").await;
|
|
cx.shared_state().await.assert_eq(indoc! { "The «quick brown
|
|
fox jumps oˇ»ver
|
|
the lazy dog"});
|
|
|
|
cx.simulate_shared_keystrokes("escape").await;
|
|
cx.shared_state().await.assert_eq(indoc! { "The quick brown
|
|
fox jumps ˇover
|
|
the lazy dog"});
|
|
|
|
// motions work backwards
|
|
cx.simulate_shared_keystrokes("v k b").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! { "The «ˇquick brown
|
|
fox jumps o»ver
|
|
the lazy dog"});
|
|
|
|
// works on empty lines
|
|
cx.set_shared_state(indoc! {"
|
|
a
|
|
ˇ
|
|
b
|
|
"})
|
|
.await;
|
|
let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
«
|
|
ˇ»b
|
|
"});
|
|
cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
|
|
|
|
// toggles off again
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
ˇ
|
|
b
|
|
"});
|
|
|
|
// works at the end of a document
|
|
cx.set_shared_state(indoc! {"
|
|
a
|
|
b
|
|
ˇ"})
|
|
.await;
|
|
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
b
|
|
ˇ"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"«The quick brown
|
|
fox jumps over
|
|
the lazy dogˇ»"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("g shift-i");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"ˇThe quick brown
|
|
ˇfox jumps over
|
|
ˇthe lazy dog"
|
|
},
|
|
Mode::Insert,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"«The quick brown
|
|
fox jumps over
|
|
the lazy dogˇ»"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("g shift-a");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"The quick brownˇ
|
|
fox jumps overˇ
|
|
the lazy dogˇ"
|
|
},
|
|
Mode::Insert,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! { "The «qˇ»uick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
cx.simulate_shared_keystrokes("x").await;
|
|
cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
|
|
the lazy dog"});
|
|
|
|
// it should work on empty lines
|
|
cx.set_shared_state(indoc! {"
|
|
a
|
|
ˇ
|
|
b"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
«
|
|
ˇ»b"});
|
|
cx.simulate_shared_keystrokes("x").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
ˇb"});
|
|
|
|
// it should work at the end of the document
|
|
cx.set_shared_state(indoc! {"
|
|
a
|
|
b
|
|
ˇ"})
|
|
.await;
|
|
let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
|
|
cx.simulate_shared_keystrokes("shift-v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
b
|
|
ˇ"});
|
|
cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
|
|
cx.simulate_shared_keystrokes("x").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
ˇb"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.simulate("v w", "The quick ˇbrown")
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate("v w x", "The quick ˇbrown")
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"v w j x",
|
|
indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
// Test pasting code copied on delete
|
|
cx.simulate_shared_keystrokes("j p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.simulate_at_each_offset(
|
|
"v w j x",
|
|
indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the ˇlazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(
|
|
"v b k x",
|
|
indoc! {"
|
|
The ˇquick brown
|
|
fox jumps ˇover
|
|
the ˇlazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The quˇick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v x").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// Test pasting code copied on delete
|
|
cx.simulate_shared_keystrokes("p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
the laˇzy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v x").await;
|
|
cx.shared_state().await.assert_matches();
|
|
cx.shared_clipboard().await.assert_eq("the lazy dog\n");
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The quˇick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v j x").await;
|
|
cx.shared_state().await.assert_matches();
|
|
// Test pasting code copied on delete
|
|
cx.simulate_shared_keystrokes("p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇlong line
|
|
should not
|
|
crash
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v $ x").await;
|
|
cx.shared_state().await.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("The quick ˇbrown").await;
|
|
cx.simulate_shared_keystrokes("v w y").await;
|
|
cx.shared_state().await.assert_eq("The quick ˇbrown");
|
|
cx.shared_clipboard().await.assert_eq("brown");
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v w j y").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
cx.shared_clipboard().await.assert_eq(indoc! {"
|
|
quick brown
|
|
fox jumps o"});
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
the ˇlazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v w j y").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
the ˇlazy dog"});
|
|
cx.shared_clipboard().await.assert_eq("lazy d");
|
|
cx.simulate_shared_keystrokes("shift-v y").await;
|
|
cx.shared_clipboard().await.assert_eq("the lazy dog\n");
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v b k y").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
ˇThe quick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
assert_eq!(
|
|
cx.read_from_clipboard()
|
|
.map(|item| item.text().unwrap().to_string())
|
|
.unwrap(),
|
|
"The q"
|
|
);
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The quick brown
|
|
fox ˇjumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
|
|
.await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
ˇfox jumps over
|
|
the lazy dog"});
|
|
cx.shared_clipboard()
|
|
.await
|
|
.assert_eq("fox jumps over\nthe lazy dog\n");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «qˇ»uick brown
|
|
fox jumps over
|
|
the lazy dog"
|
|
});
|
|
cx.simulate_shared_keystrokes("2 down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «qˇ»uick brown
|
|
fox «jˇ»umps over
|
|
the «lˇ»azy dog"
|
|
});
|
|
cx.simulate_shared_keystrokes("e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quicˇ»k brown
|
|
fox «jumpˇ»s over
|
|
the «lazyˇ» dog"
|
|
});
|
|
cx.simulate_shared_keystrokes("^").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"«ˇThe q»uick brown
|
|
«ˇfox j»umps over
|
|
«ˇthe l»azy dog"
|
|
});
|
|
cx.simulate_shared_keystrokes("$").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quick brownˇ»
|
|
fox «jumps overˇ»
|
|
the «lazy dogˇ»"
|
|
});
|
|
cx.simulate_shared_keystrokes("shift-f space").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quickˇ» brown
|
|
fox «jumpsˇ» over
|
|
the «lazy ˇ»dog"
|
|
});
|
|
|
|
// toggling through visual mode works as expected
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quick brown
|
|
fox jumps over
|
|
the lazy ˇ»dog"
|
|
});
|
|
cx.simulate_shared_keystrokes("ctrl-v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quickˇ» brown
|
|
fox «jumpsˇ» over
|
|
the «lazy ˇ»dog"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick
|
|
brown
|
|
fox
|
|
jumps over the
|
|
|
|
lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v down down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The«ˇ q»uick
|
|
bro«ˇwn»
|
|
foxˇ
|
|
jumps over the
|
|
|
|
lazy dog
|
|
"
|
|
});
|
|
cx.simulate_shared_keystrokes("down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «qˇ»uick
|
|
brow«nˇ»
|
|
fox
|
|
jump«sˇ» over the
|
|
|
|
lazy dog
|
|
"
|
|
});
|
|
cx.simulate_shared_keystrokes("left").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The«ˇ q»uick
|
|
bro«ˇwn»
|
|
foxˇ
|
|
jum«ˇps» over the
|
|
|
|
lazy dog
|
|
"
|
|
});
|
|
cx.simulate_shared_keystrokes("s o escape").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"Theˇouick
|
|
broo
|
|
foxo
|
|
jumo over the
|
|
|
|
lazy dog
|
|
"
|
|
});
|
|
|
|
// https://github.com/zed-industries/zed/issues/6274
|
|
cx.set_shared_state(indoc! {
|
|
"Theˇ quick brown
|
|
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("l ctrl-v j j").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «qˇ»uick brown
|
|
|
|
fox «jˇ»umps over
|
|
the lazy dog
|
|
"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v right down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quˇ»ick brown
|
|
fox «juˇ»mps over
|
|
the lazy dog
|
|
"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"ˇThe quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"«Tˇ»he quick brown
|
|
«fˇ»ox jumps over
|
|
«tˇ»he lazy dog
|
|
ˇ"
|
|
});
|
|
|
|
cx.simulate_shared_keystrokes("shift-i k escape").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"ˇkThe quick brown
|
|
kfox jumps over
|
|
kthe lazy dog
|
|
k"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"ˇThe quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"«Tˇ»he quick brown
|
|
«fˇ»ox jumps over
|
|
«tˇ»he lazy dog
|
|
ˇ"
|
|
});
|
|
cx.simulate_shared_keystrokes("c k escape").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"ˇkhe quick brown
|
|
kox jumps over
|
|
khe lazy dog
|
|
k"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("hello (in [parˇens] o)").await;
|
|
cx.simulate_shared_keystrokes("ctrl-v l").await;
|
|
cx.simulate_shared_keystrokes("a ]").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("hello (in «[parens]ˇ» o)");
|
|
cx.simulate_shared_keystrokes("i (").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("hello («in [parens] oˇ»)");
|
|
|
|
cx.set_shared_state("hello in a wˇord again.").await;
|
|
cx.simulate_shared_keystrokes("ctrl-v l i w").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("hello in a w«ordˇ» again.");
|
|
assert_eq!(cx.mode(), Mode::VisualBlock);
|
|
cx.simulate_shared_keystrokes("o a s").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("«ˇhello in a word» again.");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state("aˇbc", Mode::Normal);
|
|
cx.simulate_keystrokes("ctrl-v");
|
|
assert_eq!(cx.mode(), Mode::VisualBlock);
|
|
cx.simulate_keystrokes("cmd-shift-p escape");
|
|
assert_eq!(cx.mode(), Mode::VisualBlock);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_gn(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ a a enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes("g n").await;
|
|
cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
|
|
cx.simulate_shared_keystrokes("g n").await;
|
|
cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
|
|
cx.simulate_shared_keystrokes("escape d g n").await;
|
|
cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ a a enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes("3 g n").await;
|
|
cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ a a enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes("g shift-n").await;
|
|
cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
|
|
cx.simulate_shared_keystrokes("g shift-n").await;
|
|
cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_gl(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state("aaˇ aa\naa", Mode::Normal);
|
|
cx.simulate_keystrokes("g l");
|
|
cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
|
|
cx.simulate_keystrokes("g >");
|
|
cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ a a enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes("d g n").await;
|
|
|
|
cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
|
|
cx.simulate_shared_keystrokes(".").await;
|
|
cx.shared_state().await.assert_eq("aa ˇ aa aa");
|
|
cx.simulate_shared_keystrokes(".").await;
|
|
cx.shared_state().await.assert_eq("aa ˇ aa");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ a a enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes("c g n x escape").await;
|
|
cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
|
|
cx.simulate_shared_keystrokes(".").await;
|
|
cx.shared_state().await.assert_eq("aa x ˇx aa aa");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("aaˇ aa aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ b b enter").await;
|
|
cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
|
|
cx.simulate_shared_keystrokes("c g n x escape").await;
|
|
cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
|
|
cx.simulate_shared_keystrokes(".").await;
|
|
cx.shared_state().await.assert_eq("aaˇa aa aa aa");
|
|
|
|
cx.set_shared_state("aaˇ bb aa aa aa").await;
|
|
cx.simulate_shared_keystrokes("/ b b enter").await;
|
|
cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
|
|
cx.simulate_shared_keystrokes("c g n x escape").await;
|
|
cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
|
|
cx.simulate_shared_keystrokes(".").await;
|
|
cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v down shift-d").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"the ˇlazy dog\n"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"Theˇ•
|
|
fox•
|
|
the lazy dog
|
|
"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_shift_y(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown\n"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i w shift-y").await;
|
|
cx.shared_clipboard().await.assert_eq(indoc! {
|
|
"The quick brown\n"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_gv(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"The ˇquick brown"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i w escape g v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «quickˇ» brown"
|
|
});
|
|
|
|
cx.simulate_shared_keystrokes("o escape g v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «ˇquick» brown"
|
|
});
|
|
|
|
cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"«Thˇ»e quick brown"
|
|
});
|
|
cx.simulate_shared_keystrokes("g v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"The «ˇquick» brown"
|
|
});
|
|
cx.simulate_shared_keystrokes("g v").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"«Thˇ»e quick brown"
|
|
});
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
fiˇsh one
|
|
fish two
|
|
fish red
|
|
fish blue
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("4 g l escape escape g v");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«fishˇ» one
|
|
«fishˇ» two
|
|
«fishˇ» red
|
|
«fishˇ» blue
|
|
"},
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("y g v");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«fishˇ» one
|
|
«fishˇ» two
|
|
«fishˇ» red
|
|
«fishˇ» blue
|
|
"},
|
|
Mode::Visual,
|
|
);
|
|
}
|
|
}
|