Merge branch 'main' into test-branch
This commit is contained in:
+16
-3
@@ -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) {
|
||||
|
||||
+144
-80
@@ -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)
|
||||
}
|
||||
|
||||
+340
-712
File diff suppressed because it is too large
Load Diff
+143
-52
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+30
-85
@@ -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");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+129
-305
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
@@ -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"}]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
@@ -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}}}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
@@ -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"}]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
@@ -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"}]
|
||||
Reference in New Issue
Block a user