Merge branch 'main' into test-branch
This commit is contained in:
commit
41590ef64b
92 changed files with 4166 additions and 2520 deletions
|
@ -7,7 +7,20 @@ edition = "2021"
|
|||
path = "src/vim.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
itertools = "0.10"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
|
||||
async-compat = { version = "0.2.1", "optional" = true }
|
||||
async-trait = { version = "0.1", "optional" = true }
|
||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
|
||||
tokio = { version = "1.15", "optional" = true }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
|
||||
assets = { path = "../assets" }
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { path = "../command_palette" }
|
||||
|
@ -16,14 +29,14 @@ gpui = { path = "../gpui" }
|
|||
language = { path = "../language" }
|
||||
rope = { path = "../rope" }
|
||||
search = { path = "../search" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
itertools = "0.10"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "1.0.4"
|
||||
parking_lot = "0.11.1"
|
||||
lazy_static = "1.4"
|
||||
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
|
|
|
@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
|
|
|
@ -18,6 +18,7 @@ use crate::{
|
|||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Motion {
|
||||
Left,
|
||||
Backspace,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
|
@ -58,6 +59,7 @@ actions!(
|
|||
vim,
|
||||
[
|
||||
Left,
|
||||
Backspace,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
|
@ -74,6 +76,7 @@ impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
|||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||
|
@ -106,19 +109,21 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if let Some(Operator::Namespace(_)) = vim.active_operator() {
|
||||
vim.pop_operator(cx);
|
||||
}
|
||||
});
|
||||
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
|
||||
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||
}
|
||||
|
||||
let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
|
||||
let operator = Vim::read(cx).active_operator();
|
||||
match Vim::read(cx).state.mode {
|
||||
Mode::Normal => normal_motion(motion, cx),
|
||||
Mode::Visual { .. } => visual_motion(motion, cx),
|
||||
Mode::Normal => normal_motion(motion, operator, times, cx),
|
||||
Mode::Visual { .. } => visual_motion(motion, times, cx),
|
||||
Mode::Insert => {
|
||||
// Shouldn't execute a motion in insert mode. Ignoring
|
||||
}
|
||||
}
|
||||
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
|
||||
}
|
||||
|
||||
// Motion handling is specified here:
|
||||
|
@ -150,30 +155,32 @@ impl Motion {
|
|||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Left => (left(map, point), SelectionGoal::None),
|
||||
Down => movement::down(map, point, goal, true),
|
||||
Up => movement::up(map, point, goal, true),
|
||||
Right => (right(map, point), SelectionGoal::None),
|
||||
Left => (left(map, point, times), SelectionGoal::None),
|
||||
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||
Down => down(map, point, goal, times),
|
||||
Up => up(map, point, goal, times),
|
||||
Right => (right(map, point, times), SelectionGoal::None),
|
||||
NextWordStart { ignore_punctuation } => (
|
||||
next_word_start(map, point, ignore_punctuation),
|
||||
next_word_start(map, point, ignore_punctuation, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextWordEnd { ignore_punctuation } => (
|
||||
next_word_end(map, point, ignore_punctuation),
|
||||
next_word_end(map, point, ignore_punctuation, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousWordStart { ignore_punctuation } => (
|
||||
previous_word_start(map, point, ignore_punctuation),
|
||||
previous_word_start(map, point, ignore_punctuation, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
|
||||
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
|
||||
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
|
||||
Matching => (matching(map, point), SelectionGoal::None),
|
||||
}
|
||||
|
@ -184,9 +191,10 @@ impl Motion {
|
|||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
expand_to_surrounding_newline: bool,
|
||||
) {
|
||||
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
||||
let (head, goal) = self.move_point(map, selection.head(), selection.goal, times);
|
||||
selection.set_head(head, goal);
|
||||
|
||||
if self.linewise() {
|
||||
|
@ -206,7 +214,7 @@ impl Motion {
|
|||
}
|
||||
}
|
||||
|
||||
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
||||
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
|
||||
} else {
|
||||
// If the motion is exclusive and the end of the motion is in column 1, the
|
||||
// end of the motion is moved to the end of the previous line and the motion
|
||||
|
@ -234,95 +242,151 @@ impl Motion {
|
|||
}
|
||||
}
|
||||
|
||||
fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
fn next_word_start(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> DisplayPoint {
|
||||
let mut crossed_newline = false;
|
||||
movement::find_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
if at_newline {
|
||||
crossed_newline = true;
|
||||
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
for _ in 0..times {
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
point = map.clip_point(point, Bias::Right);
|
||||
if point.column() == 0 {
|
||||
break;
|
||||
}
|
||||
found
|
||||
})
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
for _ in 0..times {
|
||||
point = movement::left(map, point);
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
fn down(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::down(map, point, goal, true);
|
||||
}
|
||||
(point, goal)
|
||||
}
|
||||
|
||||
fn up(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::up(map, point, goal, true);
|
||||
}
|
||||
(point, goal)
|
||||
}
|
||||
|
||||
pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
for _ in 0..times {
|
||||
let mut new_point = point;
|
||||
*new_point.column_mut() += 1;
|
||||
let new_point = map.clip_point(new_point, Bias::Right);
|
||||
if point == new_point {
|
||||
break;
|
||||
}
|
||||
point = new_point;
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
pub(crate) fn next_word_start(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
for _ in 0..times {
|
||||
let mut crossed_newline = false;
|
||||
point = movement::find_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
if at_newline {
|
||||
crossed_newline = true;
|
||||
}
|
||||
found
|
||||
})
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
fn next_word_end(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
point = movement::find_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
*point.column_mut() += 1;
|
||||
point = movement::find_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind && !left.is_whitespace()
|
||||
});
|
||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||
// we have backtraced already
|
||||
if !map
|
||||
.chars_at(point)
|
||||
.nth(1)
|
||||
.map(|c| c == '\n')
|
||||
.unwrap_or(true)
|
||||
{
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||
});
|
||||
|
||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||
// we have backtraced already
|
||||
if !map
|
||||
.chars_at(point)
|
||||
.nth(1)
|
||||
.map(|(c, _)| c == '\n')
|
||||
.unwrap_or(true)
|
||||
{
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
}
|
||||
point = map.clip_point(point, Bias::Left);
|
||||
}
|
||||
map.clip_point(point, Bias::Left)
|
||||
point
|
||||
}
|
||||
|
||||
fn previous_word_start(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||
// cursor because the newline is checked only once.
|
||||
point = movement::find_preceding_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
for _ in 0..times {
|
||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||
// cursor because the newline is checked only once.
|
||||
point = movement::find_preceding_boundary(map, point, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||
});
|
||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||
});
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let mut column = 0;
|
||||
for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
|
||||
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
|
||||
let mut last_point = DisplayPoint::new(from.row(), 0);
|
||||
for (ch, point) in map.chars_at(last_point) {
|
||||
if ch == '\n' {
|
||||
return point;
|
||||
return from;
|
||||
}
|
||||
|
||||
last_point = point;
|
||||
|
||||
if char_kind(ch) != CharKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
|
||||
column += ch.len_utf8() as u32;
|
||||
}
|
||||
|
||||
*point.column_mut() = column;
|
||||
map.clip_point(point, Bias::Left)
|
||||
map.clip_point(last_point, Bias::Left)
|
||||
}
|
||||
|
||||
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
|
@ -333,8 +397,8 @@ fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
|||
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
|
||||
}
|
||||
|
||||
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let mut new_point = 0usize.to_display_point(map);
|
||||
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
|
||||
let mut new_point = (line - 1).to_display_point(map);
|
||||
*new_point.column_mut() = point.column();
|
||||
map.clip_point(new_point, Bias::Left)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,30 +1,20 @@
|
|||
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
||||
use editor::{char_kind, movement, Autoscroll};
|
||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
|
||||
use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
|
||||
use gpui::MutableAppContext;
|
||||
use language::Selection;
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangeWord {
|
||||
#[serde(default)]
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [ChangeWord]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(change_word);
|
||||
}
|
||||
|
||||
pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, 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.expand_selection(map, selection, false);
|
||||
if let Motion::NextWordStart { ignore_punctuation } = motion {
|
||||
expand_changed_word_selection(map, selection, times, ignore_punctuation);
|
||||
} else {
|
||||
motion.expand_selection(map, selection, times, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
|
@ -34,43 +24,60 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||
vim.switch_mode(Mode::Insert, false, cx)
|
||||
}
|
||||
|
||||
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||
let mut objects_found = false;
|
||||
vim.update_active_editor(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.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(editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if objects_found {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
} else {
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// From the docs https://vimhelp.org/change.txt.html#cw
|
||||
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
|
||||
// white space after a word, they only change up to the end of the word. This is
|
||||
// because Vim interprets "cw" as change-word, and a word does not include the
|
||||
// following white space.
|
||||
fn change_word(
|
||||
_: &mut Workspace,
|
||||
&ChangeWord { ignore_punctuation }: &ChangeWord,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
ignore_punctuation: bool,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, 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| {
|
||||
if selection.end.column() == map.line_len(selection.end.row()) {
|
||||
return;
|
||||
}
|
||||
if times > 1 {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times - 1,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
selection.end =
|
||||
movement::find_boundary(map, selection.end, |left, right| {
|
||||
let left_kind =
|
||||
char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind =
|
||||
char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
|
||||
return;
|
||||
}
|
||||
|
||||
left_kind != right_kind || left == '\n' || right == '\n'
|
||||
});
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
selection.end = movement::find_boundary(map, selection.end, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind || left == '\n' || right == '\n'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -78,7 +85,10 @@ fn change_word(
|
|||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
|
@ -170,8 +180,7 @@ mod test {
|
|||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ˇ"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-e"]);
|
||||
|
@ -193,6 +202,7 @@ mod test {
|
|||
Test ˇ
|
||||
test"},
|
||||
);
|
||||
println!("Marker");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
@ -442,4 +452,85 @@ mod test {
|
|||
the lazy"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "j"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "l"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// Changing back any number of times from the start of the file doesn't
|
||||
// switch to insert mode in vim. This is weird and painful to implement
|
||||
cx.add_initial_state_exemption(indoc! {"
|
||||
ˇThe quick brown
|
||||
|
||||
fox jumps-over
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "b"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "e"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
||||
use collections::HashMap;
|
||||
use editor::{Autoscroll, Bias};
|
||||
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
@ -11,8 +11,8 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_head = selection.head();
|
||||
motion.expand_selection(map, selection, true);
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
motion.expand_selection(map, selection, times, true);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
|
@ -36,11 +36,67 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |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 contains_only_newlines = map
|
||||
.chars_at(selection.start)
|
||||
.take_while(|(_, p)| p < &selection.end)
|
||||
.all(|(char, _)| char == '\n')
|
||||
&& !offset_range.is_empty();
|
||||
let end_at_newline = map
|
||||
.chars_at(selection.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 {
|
||||
selection.end =
|
||||
(offset_range.end + '\n'.len_utf8()).to_display_point(map);
|
||||
} else if selection.start.row() > 0 {
|
||||
should_move_to_start.insert(selection.id);
|
||||
selection.start =
|
||||
(offset_range.start - '\n'.len_utf8()).to_display_point(map);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
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)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||
|
@ -140,8 +196,7 @@ mod test {
|
|||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ˇ"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-e"]);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
||||
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||
use collections::HashMap;
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
@ -10,8 +10,8 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
motion.expand_selection(map, selection, true);
|
||||
original_positions.insert(selection.id, original_position);
|
||||
motion.expand_selection(map, selection, times, true);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
|
@ -24,3 +24,26 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |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);
|
||||
});
|
||||
});
|
||||
copy_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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
640
crates/vim/src/object.rs
Normal file
640
crates/vim/src/object.rs
Normal file
|
@ -0,0 +1,640 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
|
||||
use gpui::{actions, impl_actions, MutableAppContext};
|
||||
use language::Selection;
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum Object {
|
||||
Word { ignore_punctuation: bool },
|
||||
Sentence,
|
||||
Quotes,
|
||||
BackQuotes,
|
||||
DoubleQuotes,
|
||||
Parentheses,
|
||||
SquareBrackets,
|
||||
CurlyBrackets,
|
||||
AngleBrackets,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Word {
|
||||
#[serde(default)]
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
Sentence,
|
||||
Quotes,
|
||||
BackQuotes,
|
||||
DoubleQuotes,
|
||||
Parentheses,
|
||||
SquareBrackets,
|
||||
CurlyBrackets,
|
||||
AngleBrackets
|
||||
]
|
||||
);
|
||||
impl_actions!(vim, [Word]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
|
||||
object(Object::Word { ignore_punctuation }, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
|
||||
object(Object::SquareBrackets, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
|
||||
}
|
||||
|
||||
fn object(object: Object, cx: &mut MutableAppContext) {
|
||||
match Vim::read(cx).state.mode {
|
||||
Mode::Normal => normal_object(object, cx),
|
||||
Mode::Visual { .. } => visual_object(object, cx),
|
||||
Mode::Insert => {
|
||||
// Shouldn't execute a text object in insert mode. Ignoring
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn range(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
match self {
|
||||
Object::Word { ignore_punctuation } => {
|
||||
if around {
|
||||
around_word(map, relative_to, ignore_punctuation)
|
||||
} else {
|
||||
in_word(map, relative_to, ignore_punctuation)
|
||||
}
|
||||
}
|
||||
Object::Sentence => sentence(map, relative_to, around),
|
||||
Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''),
|
||||
Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'),
|
||||
Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'),
|
||||
Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'),
|
||||
Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'),
|
||||
Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'),
|
||||
Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_selection(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> bool {
|
||||
if let Some(range) = self.range(map, selection.head(), around) {
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a range that surrounds the word relative_to is in
|
||||
/// If relative_to is at the start of a word, return the word.
|
||||
/// If relative_to is between words, return the space between
|
||||
fn in_word(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
// Use motion::right so that we consider the character under the cursor when looking for the start
|
||||
let start = movement::find_preceding_boundary_in_line(
|
||||
map,
|
||||
right(map, relative_to, 1),
|
||||
|left, right| {
|
||||
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||
},
|
||||
);
|
||||
let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
|
||||
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||
});
|
||||
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
/// Return a range that surrounds the word and following whitespace
|
||||
/// relative_to is in.
|
||||
/// If relative_to is at the start of a word, return the word and following whitespace.
|
||||
/// If relative_to is between words, return the whitespace back and the following word
|
||||
|
||||
/// if in word
|
||||
/// delete that word
|
||||
/// if there is whitespace following the word, delete that as well
|
||||
/// otherwise, delete any preceding whitespace
|
||||
/// otherwise
|
||||
/// delete whitespace around cursor
|
||||
/// delete word following the cursor
|
||||
fn around_word(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let in_word = map
|
||||
.chars_at(relative_to)
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(c) != CharKind::Whitespace)
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_word {
|
||||
around_containing_word(map, relative_to, ignore_punctuation)
|
||||
} else {
|
||||
around_next_word(map, relative_to, ignore_punctuation)
|
||||
}
|
||||
}
|
||||
|
||||
fn around_containing_word(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
in_word(map, relative_to, ignore_punctuation)
|
||||
.map(|range| expand_to_include_whitespace(map, range, true))
|
||||
}
|
||||
|
||||
fn around_next_word(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
// Get the start of the word
|
||||
let start = movement::find_preceding_boundary_in_line(
|
||||
map,
|
||||
right(map, relative_to, 1),
|
||||
|left, right| {
|
||||
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||
},
|
||||
);
|
||||
|
||||
let mut word_found = false;
|
||||
let end = movement::find_boundary(map, relative_to, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
||||
|
||||
if right_kind != CharKind::Whitespace {
|
||||
word_found = true;
|
||||
}
|
||||
|
||||
found
|
||||
});
|
||||
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
fn sentence(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let mut start = None;
|
||||
let mut previous_end = relative_to;
|
||||
|
||||
let mut chars = map.chars_at(relative_to).peekable();
|
||||
|
||||
// Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
|
||||
for (char, point) in chars
|
||||
.peek()
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.chain(map.reverse_chars_at(relative_to))
|
||||
{
|
||||
if is_sentence_end(map, point) {
|
||||
break;
|
||||
}
|
||||
|
||||
if is_possible_sentence_start(char) {
|
||||
start = Some(point);
|
||||
}
|
||||
|
||||
previous_end = point;
|
||||
}
|
||||
|
||||
// Search forward for the end of the current sentence or if we are between sentences, the start of the next one
|
||||
let mut end = relative_to;
|
||||
for (char, point) in chars {
|
||||
if start.is_none() && is_possible_sentence_start(char) {
|
||||
if around {
|
||||
start = Some(point);
|
||||
continue;
|
||||
} else {
|
||||
end = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
end = point;
|
||||
*end.column_mut() += char.len_utf8() as u32;
|
||||
end = map.clip_point(end, Bias::Left);
|
||||
|
||||
if is_sentence_end(map, end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut range = start.unwrap_or(previous_end)..end;
|
||||
if around {
|
||||
range = expand_to_include_whitespace(map, range, false);
|
||||
}
|
||||
|
||||
Some(range)
|
||||
}
|
||||
|
||||
fn is_possible_sentence_start(character: char) -> bool {
|
||||
!character.is_whitespace() && character != '.'
|
||||
}
|
||||
|
||||
const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
|
||||
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
|
||||
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
|
||||
fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
let mut next_chars = map.chars_at(point).peekable();
|
||||
if let Some((char, _)) = next_chars.next() {
|
||||
// We are at a double newline. This position is a sentence end.
|
||||
if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The next text is not a valid whitespace. This is not a sentence end
|
||||
if !SENTENCE_END_WHITESPACE.contains(&char) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (char, _) in map.reverse_chars_at(point) {
|
||||
if SENTENCE_END_PUNCTUATION.contains(&char) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !SENTENCE_END_FILLERS.contains(&char) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
||||
/// whitespace to the end first and falls back to the start if there was none.
|
||||
fn expand_to_include_whitespace(
|
||||
map: &DisplaySnapshot,
|
||||
mut range: Range<DisplayPoint>,
|
||||
stop_at_newline: bool,
|
||||
) -> Range<DisplayPoint> {
|
||||
let mut whitespace_included = false;
|
||||
|
||||
let mut chars = map.chars_at(range.end).peekable();
|
||||
while let Some((char, point)) = chars.next() {
|
||||
if char == '\n' && stop_at_newline {
|
||||
break;
|
||||
}
|
||||
|
||||
if char.is_whitespace() {
|
||||
// Set end to the next display_point or the character position after the current display_point
|
||||
range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
|
||||
let mut end = point;
|
||||
*end.column_mut() += char.len_utf8() as u32;
|
||||
map.clip_point(end, Bias::Left)
|
||||
});
|
||||
|
||||
if char != '\n' {
|
||||
whitespace_included = true;
|
||||
}
|
||||
} else {
|
||||
// Found non whitespace. Quit out.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !whitespace_included {
|
||||
for (char, point) in map.reverse_chars_at(range.start) {
|
||||
if char == '\n' && stop_at_newline {
|
||||
break;
|
||||
}
|
||||
|
||||
if !char.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
|
||||
range.start = point;
|
||||
}
|
||||
}
|
||||
|
||||
range
|
||||
}
|
||||
|
||||
fn surrounding_markers(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
around: bool,
|
||||
search_across_lines: bool,
|
||||
start_marker: char,
|
||||
end_marker: char,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let mut matched_ends = 0;
|
||||
let mut start = None;
|
||||
for (char, mut point) in map.reverse_chars_at(relative_to) {
|
||||
if char == start_marker {
|
||||
if matched_ends > 0 {
|
||||
matched_ends -= 1;
|
||||
} else {
|
||||
if around {
|
||||
start = Some(point)
|
||||
} else {
|
||||
*point.column_mut() += char.len_utf8() as u32;
|
||||
start = Some(point);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if char == end_marker {
|
||||
matched_ends += 1;
|
||||
} else if char == '\n' && !search_across_lines {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut matched_starts = 0;
|
||||
let mut end = None;
|
||||
for (char, mut point) in map.chars_at(relative_to) {
|
||||
if char == end_marker {
|
||||
if start.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
if matched_starts > 0 {
|
||||
matched_starts -= 1;
|
||||
} else {
|
||||
if around {
|
||||
*point.column_mut() += char.len_utf8() as u32;
|
||||
end = Some(point);
|
||||
} else {
|
||||
end = Some(point);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if char == start_marker {
|
||||
if start.is_none() {
|
||||
if around {
|
||||
start = Some(point);
|
||||
} else {
|
||||
*point.column_mut() += char.len_utf8() as u32;
|
||||
start = Some(point);
|
||||
}
|
||||
} else {
|
||||
matched_starts += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if char == '\n' && !search_across_lines {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(start), Some(end)) = (start, end) {
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
const WORD_LOCATIONS: &'static str = indoc! {"
|
||||
The quick ˇbrowˇnˇ
|
||||
fox ˇjuˇmpsˇ over
|
||||
the lazy dogˇ
|
||||
ˇ
|
||||
ˇ
|
||||
ˇ
|
||||
Thˇeˇ-ˇquˇickˇ ˇbrownˇ
|
||||
ˇ
|
||||
ˇ
|
||||
ˇ fox-jumpˇs over
|
||||
the lazy dogˇ
|
||||
ˇ
|
||||
"};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
// Visual text objects are slightly broken when used with non empty selections
|
||||
// cx.assert_binding_matches_all(["v", "h", "i", "w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
// cx.assert_binding_matches_all(["v", "l", "i", "w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
|
||||
.await;
|
||||
|
||||
// Visual text objects are slightly broken when used with non empty selections
|
||||
// cx.assert_binding_matches_all(["v", "i", "h", "shift-w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
// cx.assert_binding_matches_all(["v", "i", "l", "shift-w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
|
||||
// Visual around words is somewhat broken right now when it comes to newlines
|
||||
// cx.assert_binding_matches_all(["v", "a", "w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
// cx.assert_binding_matches_all(["v", "a", "shift-w"], WORD_LOCATIONS)
|
||||
// .await;
|
||||
}
|
||||
|
||||
const SENTENCE_EXAMPLES: &[&'static str] = &[
|
||||
"ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
|
||||
indoc! {"
|
||||
ˇThe quick ˇbrownˇ
|
||||
fox jumps over
|
||||
the lazy doˇgˇ.ˇ ˇThe quick ˇ
|
||||
brown fox jumps over
|
||||
"},
|
||||
// Position of the cursor after deletion between lines isn't quite right.
|
||||
// Deletion in a sentence at the start of a line with whitespace is incorrect.
|
||||
// indoc! {"
|
||||
// The quick brown fox jumps.
|
||||
// Over the lazy dog
|
||||
// ˇ
|
||||
// ˇ
|
||||
// ˇ fox-jumpˇs over
|
||||
// the lazy dog.ˇ
|
||||
// ˇ
|
||||
// "},
|
||||
r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
|
||||
];
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["c", "i", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
|
||||
let mut cx = cx.binding(["c", "a", "s"]);
|
||||
// Resulting position is slightly incorrect for unintuitive reasons.
|
||||
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
|
||||
// Changing around the sentence at the end of the line doesn't remove whitespace.'
|
||||
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
|
||||
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["d", "i", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
|
||||
let mut cx = cx.binding(["d", "a", "s"]);
|
||||
// Resulting position is slightly incorrect for unintuitive reasons.
|
||||
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
|
||||
// Changing around the sentence at the end of the line doesn't remove whitespace.'
|
||||
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
|
||||
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["v", "i", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
|
||||
// Visual around sentences is somewhat broken right now when it comes to newlines
|
||||
// let mut cx = cx.binding(["d", "a", "s"]);
|
||||
// for sentence_example in SENTENCE_EXAMPLES {
|
||||
// cx.assert_all(sentence_example).await;
|
||||
// }
|
||||
}
|
||||
|
||||
// Test string with "`" for opening surrounders and "'" for closing surrounders
|
||||
const SURROUNDING_MARKER_STRING: &str = indoc! {"
|
||||
ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
|
||||
'ˇfox juˇmps ovˇ`ˇer
|
||||
the ˇlazy dˇ'ˇoˇ`ˇg"};
|
||||
|
||||
const SURROUNDING_OBJECTS: &[(char, char)] = &[
|
||||
// ('\'', '\''), // Quote,
|
||||
// ('`', '`'), // Back Quote
|
||||
// ('"', '"'), // Double Quote
|
||||
// ('"', '"'), // Double Quote
|
||||
('(', ')'), // Parentheses
|
||||
('[', ']'), // SquareBrackets
|
||||
('{', '}'), // CurlyBrackets
|
||||
('<', '>'), // AngleBrackets
|
||||
];
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for (start, end) in SURROUNDING_OBJECTS {
|
||||
let marked_string = SURROUNDING_MARKER_STRING
|
||||
.replace('`', &start.to_string())
|
||||
.replace('\'', &end.to_string());
|
||||
|
||||
// cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
|
||||
// .await;
|
||||
cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
|
||||
.await;
|
||||
// cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
|
||||
// .await;
|
||||
cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for (start, end) in SURROUNDING_OBJECTS {
|
||||
let marked_string = SURROUNDING_MARKER_STRING
|
||||
.replace('`', &start.to_string())
|
||||
.replace('\'', &end.to_string());
|
||||
|
||||
// cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
|
||||
// .await;
|
||||
cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
|
||||
.await;
|
||||
// cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
|
||||
// .await;
|
||||
cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use editor::CursorShape;
|
||||
use gpui::keymap::Context;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
|
@ -22,10 +22,12 @@ pub enum Namespace {
|
|||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum Operator {
|
||||
Number(usize),
|
||||
Namespace(Namespace),
|
||||
Change,
|
||||
Delete,
|
||||
Yank,
|
||||
Object { around: bool },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -77,7 +79,12 @@ impl VimState {
|
|||
context.set.insert("VimControl".to_string());
|
||||
}
|
||||
|
||||
Operator::set_context(self.operator_stack.last(), &mut context);
|
||||
let active_operator = self.operator_stack.last();
|
||||
if matches!(active_operator, Some(Operator::Object { .. })) {
|
||||
context.set.insert("VimObject".to_string());
|
||||
}
|
||||
|
||||
Operator::set_context(active_operator, &mut context);
|
||||
|
||||
context
|
||||
}
|
||||
|
@ -86,10 +93,14 @@ impl VimState {
|
|||
impl Operator {
|
||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
||||
let operator_context = match operator {
|
||||
Some(Operator::Number(_)) => "n",
|
||||
Some(Operator::Namespace(Namespace::G)) => "g",
|
||||
Some(Operator::Object { around: false }) => "i",
|
||||
Some(Operator::Object { around: true }) => "a",
|
||||
Some(Operator::Change) => "c",
|
||||
Some(Operator::Delete) => "d",
|
||||
Some(Operator::Yank) => "y",
|
||||
|
||||
None => "none",
|
||||
}
|
||||
.to_owned();
|
||||
|
|
103
crates/vim/src/test.rs
Normal file
103
crates/vim/src/test.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
mod neovim_backed_binding_test_context;
|
||||
mod neovim_backed_test_context;
|
||||
mod neovim_connection;
|
||||
mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_binding_test_context::*;
|
||||
pub use vim_test_context::*;
|
||||
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate_shared_keystroke("i").await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.simulate_shared_keystrokes([
|
||||
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
|
||||
])
|
||||
.await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.assert_editor_state("ˇtest");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.blur());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.focus_self());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hˇjkl");
|
||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hTestˇjkl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("/");
|
||||
|
||||
// We now use a weird insert mode with selection when jumping to a single line editor
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
})
|
||||
}
|
80
crates/vim/src/test/neovim_backed_binding_test_context.rs
Normal file
80
crates/vim/src/test/neovim_backed_binding_test_context.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use gpui::ContextHandle;
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
use super::NeovimBackedTestContext;
|
||||
|
||||
pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: NeovimBackedTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
cx: NeovimBackedTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(self) -> NeovimBackedTestContext<'a> {
|
||||
self.cx
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; NEW_COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
|
||||
self.consume().binding(keystrokes)
|
||||
}
|
||||
|
||||
pub async fn assert(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
) -> Option<(ContextHandle, ContextHandle)> {
|
||||
self.cx
|
||||
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn assert_manual(
|
||||
&mut self,
|
||||
initial_state: &str,
|
||||
mode_before: Mode,
|
||||
state_after: &str,
|
||||
mode_after: Mode,
|
||||
) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
mode_before,
|
||||
state_after,
|
||||
mode_after,
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn assert_all(&mut self, marked_positions: &str) {
|
||||
self.cx
|
||||
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
type Target = NeovimBackedTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::ContextHandle;
|
||||
use language::OffsetRangeExt;
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
|
||||
pub struct NeovimBackedTestContext<'a> {
|
||||
cx: VimTestContext<'a>,
|
||||
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||
neovim: NeovimConnection,
|
||||
}
|
||||
|
||||
impl<'a> NeovimBackedTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||
let function_name = cx.function_name.clone();
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
Self {
|
||||
cx,
|
||||
exemptions: Default::default(),
|
||||
neovim: NeovimConnection::new(function_name).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
|
||||
let initial_state = initial_state.to_string();
|
||||
// None represents all keybindings being exempted for that initial state
|
||||
self.exemptions.insert(initial_state, None);
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
self.simulate_keystroke(keystroke_text)
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) -> ContextHandle {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
}
|
||||
self.simulate_keystrokes(keystroke_texts)
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||
let context_handle = self.set_state(marked_text, Mode::Normal);
|
||||
|
||||
let selection = self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||
let text = self.buffer_text();
|
||||
self.neovim.set_state(selection, &text).await;
|
||||
|
||||
context_handle
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
assert_eq!(
|
||||
self.neovim.text().await,
|
||||
self.buffer_text(),
|
||||
"{}",
|
||||
self.assertion_context()
|
||||
);
|
||||
|
||||
let mut neovim_selection = self.neovim.selection().await;
|
||||
// Zed selections adjust themselves to make the end point visually make sense
|
||||
if neovim_selection.start > neovim_selection.end {
|
||||
neovim_selection.start.column += 1;
|
||||
}
|
||||
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
|
||||
self.assert_editor_selections(vec![neovim_selection]);
|
||||
|
||||
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
) -> Option<(ContextHandle, ContextHandle)> {
|
||||
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
|
||||
match possible_exempted_keystrokes {
|
||||
Some(exempted_keystrokes) => {
|
||||
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
|
||||
// This keystroke was exempted for this insertion text
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All keystrokes for this insertion text are exempted
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _state_context = self.set_shared_state(initial_state).await;
|
||||
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_state_matches().await;
|
||||
Some((_state_context, _keystroke_context))
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches_all<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
marked_positions: &str,
|
||||
) {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
|
||||
self.assert_binding_matches(keystrokes, &marked_text).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
NeovimBackedBindingTestContext::new(keystrokes, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for NeovimBackedTestContext<'a> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.set_shared_state("This is a tesˇt").await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
}
|
383
crates/vim/src/test/neovim_connection.rs
Normal file
383
crates/vim/src/test/neovim_connection.rs
Normal file
|
@ -0,0 +1,383 @@
|
|||
#[cfg(feature = "neovim")]
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::keymap::Keystroke;
|
||||
use language::{Point, Selection};
|
||||
#[cfg(feature = "neovim")]
|
||||
use lazy_static::lazy_static;
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
};
|
||||
#[cfg(feature = "neovim")]
|
||||
use parking_lot::ReentrantMutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::state::Mode;
|
||||
use collections::VecDeque;
|
||||
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
lazy_static! {
|
||||
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum NeovimData {
|
||||
Text(String),
|
||||
Selection { start: (u32, u32), end: (u32, u32) },
|
||||
Mode(Option<Mode>),
|
||||
}
|
||||
|
||||
pub struct NeovimConnection {
|
||||
data: VecDeque<NeovimData>,
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id: String,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: Child,
|
||||
}
|
||||
|
||||
impl NeovimConnection {
|
||||
pub async fn new(test_case_id: String) -> Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
let handler = NvimHandler {};
|
||||
#[cfg(feature = "neovim")]
|
||||
let (nvim, join_handle, child) = Compat::new(async {
|
||||
// Ensure we don't create neovim connections in parallel
|
||||
let _lock = NEOVIM_LOCK.lock();
|
||||
let (nvim, join_handle, child) = new_child_cmd(
|
||||
&mut Command::new("nvim").arg("--embed").arg("--clean"),
|
||||
handler,
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to neovim process");
|
||||
|
||||
nvim.ui_attach(100, 100, &UiAttachOptions::default())
|
||||
.await
|
||||
.expect("Could not attach to ui");
|
||||
|
||||
// Makes system act a little more like zed in terms of indentation
|
||||
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||
.await
|
||||
.expect("Could not set smartindent on startup");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
data: Default::default(),
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
data: Self::read_test_data(&test_case_id),
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a keystroke to the neovim process.
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
let special = keystroke.shift
|
||||
|| keystroke.ctrl
|
||||
|| keystroke.alt
|
||||
|| keystroke.cmd
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.ctrl { "C-" } else { "" };
|
||||
let alt = if keystroke.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.cmd { "D-" } else { "" };
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
||||
self.nvim
|
||||
.input(&key)
|
||||
.await
|
||||
.expect("Could not input keystroke");
|
||||
}
|
||||
|
||||
// If not running with a live neovim connection, this is a no-op
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let lines = text
|
||||
.split('\n')
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nvim_buffer
|
||||
.set_lines(0, -1, false, lines)
|
||||
.await
|
||||
.expect("Could not set nvim buffer text");
|
||||
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
|
||||
let nvim_window = self
|
||||
.nvim
|
||||
.get_current_win()
|
||||
.await
|
||||
.expect("Could not get neovim window");
|
||||
|
||||
if !selection.is_empty() {
|
||||
panic!("Setting neovim state with non empty selection not yet supported");
|
||||
}
|
||||
let cursor = selection.head();
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn text(&mut self) -> String {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let text = nvim_buffer
|
||||
.get_lines(0, -1, false)
|
||||
.await
|
||||
.expect("Could not get buffer text")
|
||||
.join("\n");
|
||||
|
||||
self.data.push_back(NeovimData::Text(text.clone()));
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn text(&mut self) -> String {
|
||||
if let Some(NeovimData::Text(text)) = self.data.pop_front() {
|
||||
text
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn selection(&mut self) -> Range<Point> {
|
||||
let cursor_row: u32 = self
|
||||
.nvim
|
||||
.command_output("echo line('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim rows start at 1
|
||||
let cursor_col: u32 = self
|
||||
.nvim
|
||||
.command_output("echo col('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim columns start at 1
|
||||
|
||||
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not exit visual mode");
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let (start_row, start_col) = nvim_buffer
|
||||
.get_mark("<")
|
||||
.await
|
||||
.expect("Could not get selection start");
|
||||
let (end_row, end_col) = nvim_buffer
|
||||
.get_mark(">")
|
||||
.await
|
||||
.expect("Could not get selection end");
|
||||
self.nvim
|
||||
.input("gv")
|
||||
.await
|
||||
.expect("Could not reselect visual selection");
|
||||
|
||||
if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
|
||||
(
|
||||
(end_row as u32 - 1, end_col as u32),
|
||||
(start_row as u32 - 1, start_col as u32),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(start_row as u32 - 1, start_col as u32),
|
||||
(end_row as u32 - 1, end_col as u32),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
((cursor_row, cursor_col), (cursor_row, cursor_col))
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Selection { start, end });
|
||||
|
||||
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn selection(&mut self) -> Range<Point> {
|
||||
// Selection code fetches the mode. This emulates that.
|
||||
let _mode = self.mode().await;
|
||||
if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
|
||||
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
let nvim_mode_text = self
|
||||
.nvim
|
||||
.get_mode()
|
||||
.await
|
||||
.expect("Could not get mode")
|
||||
.into_iter()
|
||||
.find_map(|(key, value)| {
|
||||
if key.as_str() == Some("mode") {
|
||||
Some(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Could not find mode value");
|
||||
|
||||
let mode = match nvim_mode_text.as_ref() {
|
||||
"i" => Some(Mode::Insert),
|
||||
"n" => Some(Mode::Normal),
|
||||
"v" => Some(Mode::Visual { line: false }),
|
||||
"V" => Some(Mode::Visual { line: true }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Mode(mode.clone()));
|
||||
|
||||
mode
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
|
||||
mode
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(format!("{}.json", test_case_id));
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let json = std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
);
|
||||
|
||||
serde_json::from_str(&json)
|
||||
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Deref for NeovimConnection {
|
||||
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl DerefMut for NeovimConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Drop for NeovimConnection {
|
||||
fn drop(&mut self) {
|
||||
let path = Self::test_data_path(&self.test_case_id);
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("Could not create test data directory");
|
||||
let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
|
||||
std::fs::write(path, json).expect("Could not write out test data");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[derive(Clone)]
|
||||
struct NvimHandler {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[async_trait]
|
||||
impl Handler for NvimHandler {
|
||||
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) -> Result<Value, Value> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn handle_notify(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) {
|
||||
}
|
||||
}
|
69
crates/vim/src/test/vim_binding_test_context.rs
Normal file
69
crates/vim/src/test/vim_binding_test_context.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::*;
|
||||
|
||||
use super::VimTestContext;
|
||||
|
||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: VimTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
cx: VimTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
mode_before,
|
||||
mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||
VimBindingTestContext {
|
||||
keystrokes_under_test,
|
||||
cx: self.cx,
|
||||
mode_before: self.mode_before,
|
||||
mode_after: self.mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||
self.mode_after = mode_after;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
self.mode_before,
|
||||
state_after,
|
||||
self.mode_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use editor::test::EditorTestContext;
|
||||
use gpui::{json::json, AppContext, ViewHandle};
|
||||
use editor::test::editor_test_context::EditorTestContext;
|
||||
use gpui::{json::json, AppContext, ContextHandle, ViewHandle};
|
||||
use project::Project;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
use workspace::{pane, AppState, WorkspaceHandle};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
use super::VimBindingTestContext;
|
||||
|
||||
pub struct VimTestContext<'a> {
|
||||
cx: EditorTestContext<'a>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
|
@ -117,18 +119,18 @@ impl<'a> VimTestContext<'a> {
|
|||
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
|
||||
self.cx.update(|cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(mode, false, cx);
|
||||
})
|
||||
});
|
||||
self.cx.set_state(text);
|
||||
self.cx.set_state(text)
|
||||
}
|
||||
|
||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||
self.assert_editor_state(text);
|
||||
assert_eq!(self.mode(), mode);
|
||||
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding<const COUNT: usize>(
|
||||
|
@ -142,8 +144,8 @@ impl<'a> VimTestContext<'a> {
|
|||
self.set_state(initial_state, initial_mode);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
assert_eq!(self.mode(), mode_after);
|
||||
assert_eq!(self.active_operator(), None);
|
||||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
|
@ -168,67 +170,3 @@ impl<'a> DerefMut for VimTestContext<'a> {
|
|||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: VimTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
cx: VimTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
mode_before,
|
||||
mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||
VimBindingTestContext {
|
||||
keystrokes_under_test,
|
||||
cx: self.cx,
|
||||
mode_before: self.mode_before,
|
||||
mode_after: self.mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||
self.mode_after = mode_after;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
self.mode_before,
|
||||
state_after,
|
||||
self.mode_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
#[cfg(test)]
|
||||
mod vim_test_context;
|
||||
mod test;
|
||||
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
mod motion;
|
||||
mod normal;
|
||||
mod object;
|
||||
mod state;
|
||||
mod utils;
|
||||
mod visual;
|
||||
|
@ -25,13 +26,17 @@ pub struct SwitchMode(pub Mode);
|
|||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct PushOperator(pub Operator);
|
||||
|
||||
impl_actions!(vim, [SwitchMode, PushOperator]);
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
struct Number(u8);
|
||||
|
||||
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
editor_events::init(cx);
|
||||
normal::init(cx);
|
||||
visual::init(cx);
|
||||
insert::init(cx);
|
||||
object::init(cx);
|
||||
motion::init(cx);
|
||||
|
||||
// Vim Actions
|
||||
|
@ -43,6 +48,9 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||
},
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
|
||||
});
|
||||
|
||||
// Editor Actions
|
||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||
|
@ -143,12 +151,31 @@ impl Vim {
|
|||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn push_number(&mut self, Number(number): &Number, cx: &mut MutableAppContext) {
|
||||
if let Some(Operator::Number(current_number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
|
||||
} else {
|
||||
self.push_operator(Operator::Number(*number as usize), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
||||
let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||
let popped_operator = self.state.operator_stack.pop()
|
||||
.expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||
self.sync_vim_settings(cx);
|
||||
popped_operator
|
||||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut MutableAppContext) -> usize {
|
||||
let mut times = 1;
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
times = number;
|
||||
self.pop_operator(cx);
|
||||
}
|
||||
times
|
||||
}
|
||||
|
||||
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
||||
self.state.operator_stack.clear();
|
||||
self.sync_vim_settings(cx);
|
||||
|
@ -204,85 +231,3 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.blur());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.focus_self());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hˇjkl");
|
||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hTestˇjkl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("/");
|
||||
|
||||
// We now use a weird insert mode with selection when jumping to a single line editor
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,13 @@ use gpui::{actions, MutableAppContext, ViewContext};
|
|||
use language::{AutoindentMode, SelectionGoal};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
utils::copy_selections_content,
|
||||
Vim,
|
||||
};
|
||||
|
||||
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
|
||||
|
||||
|
@ -17,13 +23,15 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(paste);
|
||||
}
|
||||
|
||||
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
|
||||
let was_reversed = selection.reversed;
|
||||
|
||||
let (new_head, goal) =
|
||||
motion.move_point(map, selection.head(), selection.goal, times);
|
||||
selection.set_head(new_head, goal);
|
||||
|
||||
if was_reversed && !selection.reversed {
|
||||
|
@ -43,6 +51,36 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if let Operator::Object { around } = vim.pop_operator(cx) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let head = selection.head();
|
||||
if let Some(mut range) = object.range(map, head, around) {
|
||||
if !range.is_empty() {
|
||||
if let Some((_, end)) = map.reverse_chars_at(range.end).next() {
|
||||
range.end = end;
|
||||
}
|
||||
|
||||
if selection.is_empty() {
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
} else if selection.reversed {
|
||||
selection.start = range.start;
|
||||
} else {
|
||||
selection.end = range.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
|
@ -274,365 +312,151 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
|
|||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx
|
||||
.binding(["v", "w", "j"])
|
||||
.mode_after(Mode::Visual { line: false });
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["v", "w", "j"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The «quick brown
|
||||
fox jumps ˇ»over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the «lazy ˇ»dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps «over
|
||||
ˇ»the lazy dog"},
|
||||
);
|
||||
let mut cx = cx
|
||||
.binding(["v", "b", "k"])
|
||||
.mode_after(Mode::Visual { line: false });
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["v", "b", "k"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
«ˇThe q»uick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
«ˇfox jumps over
|
||||
the l»azy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The «ˇquick brown
|
||||
fox jumps o»ver
|
||||
the lazy dog"},
|
||||
);
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["v", "w", "x"]);
|
||||
cx.assert("The quick ˇbrown", "The quickˇ ");
|
||||
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_keystrokes(["j", "p"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The ver
|
||||
the lˇquick brown
|
||||
fox jumps oazy dog"});
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇhe lazy dog"},
|
||||
);
|
||||
let mut cx = cx.binding(["v", "b", "k", "x"]);
|
||||
cx.assert(
|
||||
cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["v", "w", "j", "x"],
|
||||
indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇuick brown
|
||||
)
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_shared_keystrokes(["j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["v", "b", "k", "x"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-v", "x"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["shift-v", "x"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fox jumps over
|
||||
ˇThe quick brown
|
||||
the lazy dog"});
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
the laˇzy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over"},
|
||||
);
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-v", "j", "x"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
"the laˇzy dog",
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
the lazy dog
|
||||
ˇThe quick brown
|
||||
fox jumps over"});
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
"The quˇick brown",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over"},
|
||||
);
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert("The quick ˇbrown", "The quick ˇ");
|
||||
let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["v", "w", "c"]);
|
||||
cx.assert("The quick ˇbrown").await;
|
||||
let mut cx = cx.binding(["v", "w", "j", "c"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇhe lazy dog"},
|
||||
);
|
||||
let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["v", "b", "k", "c"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇuick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["shift-v", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on change
|
||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
fox jumps over
|
||||
ˇThe quick brown
|
||||
the lazy dog"});
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇ
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
ˇ"},
|
||||
);
|
||||
let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-v", "j", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
the lazy dog"},
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
the lazy dog
|
||||
ˇThe quick brown
|
||||
fox jumps over"});
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇ"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
ˇ"},
|
||||
);
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -741,7 +565,7 @@ mod test {
|
|||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumpsˇjumps over
|
||||
fox jumpsjumpˇs over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"This is a test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_a.json
Normal file
1
crates/vim/test_data/test_a.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_b.json
Normal file
1
crates/vim/test_data/test_b.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_backspace.json
Normal file
1
crates/vim/test_data/test_backspace.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_cc.json
Normal file
1
crates/vim/test_data/test_cc.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_change_sentence_object.json
Normal file
1
crates/vim/test_data/test_change_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_change_word_object.json
Normal file
1
crates/vim/test_data/test_change_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_dd.json
Normal file
1
crates/vim/test_data/test_dd.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_left.json
Normal file
1
crates/vim/test_data/test_delete_left.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_sentence_object.json
Normal file
1
crates/vim/test_data/test_delete_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_delete_to_end_of_line.json
Normal file
1
crates/vim/test_data/test_delete_to_end_of_line.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_word_object.json
Normal file
1
crates/vim/test_data/test_delete_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_e.json
Normal file
1
crates/vim/test_data/test_e.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_enter_visual_mode.json
Normal file
1
crates/vim/test_data/test_enter_visual_mode.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[2,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[2,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[0,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[0,4]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[1,0]}},{"Mode":{"Visual":{"line":false}}}]
|
1
crates/vim/test_data/test_gg.json
Normal file
1
crates/vim/test_data/test_gg.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_h.json
Normal file
1
crates/vim/test_data/test_h.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_insert_end_of_line.json
Normal file
1
crates/vim/test_data/test_insert_end_of_line.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"}]
|
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_insert_line_above.json
Normal file
1
crates/vim/test_data/test_insert_line_above.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_j.json
Normal file
1
crates/vim/test_data/test_j.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_jump_to_end.json
Normal file
1
crates/vim/test_data/test_jump_to_end.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,16],"end":[3,16]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick\n\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
|
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_jump_to_line_boundaries.json
Normal file
1
crates/vim/test_data/test_jump_to_line_boundaries.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_k.json
Normal file
1
crates/vim/test_data/test_k.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_l.json
Normal file
1
crates/vim/test_data/test_l.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,1],"end":[1,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_neovim.json
Normal file
1
crates/vim/test_data/test_neovim.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_o.json
Normal file
1
crates/vim/test_data/test_o.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"fn test() {\n println!();\n \n}\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"fn test() {\n\n println!();\n}"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_p.json
Normal file
1
crates/vim/test_data/test_p.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps overjumps o\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,20],"end":[1,20]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_repeated_cb.json
Normal file
1
crates/vim/test_data/test_repeated_cb.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_ce.json
Normal file
1
crates/vim/test_data/test_repeated_ce.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_cj.json
Normal file
1
crates/vim/test_data/test_repeated_cj.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_cl.json
Normal file
1
crates/vim/test_data/test_repeated_cl.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_word.json
Normal file
1
crates/vim/test_data/test_repeated_word.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_visual_change.json
Normal file
1
crates/vim/test_data/test_visual_change.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_visual_delete.json
Normal file
1
crates/vim/test_data/test_visual_delete.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick "},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The ver\nthe lquick brown\nfox jumps oazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_visual_line_change.json
Normal file
1
crates/vim/test_data/test_visual_line_change.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_visual_line_delete.json
Normal file
1
crates/vim/test_data/test_visual_line_delete.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"fox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"fox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"the lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_visual_sentence_object.json
Normal file
1
crates/vim/test_data/test_visual_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_visual_word_object.json
Normal file
1
crates/vim/test_data/test_visual_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_w.json
Normal file
1
crates/vim/test_data/test_w.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_x.json
Normal file
1
crates/vim/test_data/test_x.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
|
Loading…
Add table
Add a link
Reference in a new issue