mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Adds word and sentence text objects along with a new vim testing system which uses cached neovim data to verify our test accuracy
This commit is contained in:
parent
e96abf1429
commit
b82db3a254
1197
Cargo.lock
generated
1197
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,23 @@
|
||||
}
|
||||
],
|
||||
"%": "vim::Matching",
|
||||
"escape": "editor::Cancel"
|
||||
"escape": "editor::Cancel",
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -134,6 +150,20 @@
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": [
|
||||
"vim::Word",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
|
@ -330,34 +330,91 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(self.blocks_snapshot.max_point())
|
||||
}
|
||||
|
||||
/// Returns text chunks starting at the given display row until the end of the file
|
||||
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.blocks_snapshot
|
||||
.chunks(display_row..self.max_point().row() + 1, false, None)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.blocks_snapshot
|
||||
.chunks(row..row + 1, false, None)
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
||||
self.blocks_snapshot
|
||||
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
||||
}
|
||||
|
||||
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
|
||||
let mut column = 0;
|
||||
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
|
||||
while column < point.column() {
|
||||
if let Some(c) = chars.next() {
|
||||
column += c.len_utf8() as u32;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chars
|
||||
pub fn chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||
self.text_chunks(point.row())
|
||||
.flat_map(str::chars)
|
||||
.skip_while({
|
||||
let mut column = 0;
|
||||
move |char| {
|
||||
let at_point = column >= point.column();
|
||||
column += char.len_utf8() as u32;
|
||||
!at_point
|
||||
}
|
||||
})
|
||||
.map(move |ch| {
|
||||
let result = (ch, point);
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reverse_chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||
self.reverse_text_chunks(point.row())
|
||||
.flat_map(|chunk| chunk.chars().rev())
|
||||
.skip_while({
|
||||
let mut column = self.line_len(point.row());
|
||||
if self.max_point().row() > point.row() {
|
||||
column += 1;
|
||||
}
|
||||
|
||||
move |char| {
|
||||
let at_point = column <= point.column();
|
||||
column = column.saturating_sub(char.len_utf8() as u32);
|
||||
!at_point
|
||||
}
|
||||
})
|
||||
.map(move |ch| {
|
||||
if ch == '\n' {
|
||||
*point.row_mut() -= 1;
|
||||
*point.column_mut() = self.line_len(point.row());
|
||||
} else {
|
||||
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
|
||||
}
|
||||
(ch, point)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if column >= target {
|
||||
break;
|
||||
}
|
||||
@ -370,7 +427,7 @@ impl DisplaySnapshot {
|
||||
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
||||
let mut column = 0;
|
||||
|
||||
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||
if c == '\n' || count >= char_count as usize {
|
||||
break;
|
||||
}
|
||||
@ -454,7 +511,7 @@ impl DisplaySnapshot {
|
||||
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
||||
let mut indent = 0;
|
||||
let mut is_blank = true;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if c == ' ' {
|
||||
indent += 1;
|
||||
} else {
|
||||
|
@ -4074,7 +4074,7 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::line_beginning(map, head, true),
|
||||
movement::indented_line_beginning(map, head, true),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
@ -4089,7 +4089,7 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
|
@ -752,7 +752,7 @@ impl EditorElement {
|
||||
.snapshot
|
||||
.chars_at(cursor_position)
|
||||
.next()
|
||||
.and_then(|character| {
|
||||
.and_then(|(character, _)| {
|
||||
let font_id =
|
||||
cursor_row_layout.font_for_index(cursor_column)?;
|
||||
let text = character.to_string();
|
||||
|
@ -101,6 +101,22 @@ pub fn line_beginning(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
stop_at_soft_boundaries: bool,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||
let line_start = map.prev_line_boundary(point).1;
|
||||
|
||||
if stop_at_soft_boundaries && display_point != soft_line_start {
|
||||
soft_line_start
|
||||
} else {
|
||||
line_start
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indented_line_beginning(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
stop_at_soft_boundaries: bool,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||
@ -167,54 +183,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||
})
|
||||
}
|
||||
|
||||
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
|
||||
/// is found, indicated by the given predicate returning true. The predicate is called with the
|
||||
/// character to the left and right of the candidate boundary location, and will be called with `\n`
|
||||
/// characters indicating the start or end of a line. If the predicate returns true multiple times
|
||||
/// on a line, the *rightmost* boundary is returned.
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line.
|
||||
pub fn find_preceding_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
end: DisplayPoint,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut point = end;
|
||||
loop {
|
||||
*point.column_mut() = 0;
|
||||
if point.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
*point.column_mut() = indent;
|
||||
let mut start_column = 0;
|
||||
let mut soft_wrap_row = from.row() + 1;
|
||||
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.reverse_chars_at(from) {
|
||||
// Recompute soft_wrap_indent if the row has changed
|
||||
if point.row() != soft_wrap_row {
|
||||
soft_wrap_row = point.row();
|
||||
|
||||
if point.row() == 0 {
|
||||
start_column = 0;
|
||||
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
start_column = indent;
|
||||
}
|
||||
}
|
||||
|
||||
let mut boundary = None;
|
||||
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
|
||||
for ch in map.chars_at(point) {
|
||||
if point >= end {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
boundary = Some(point);
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
|
||||
prev_ch = Some(ch);
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
// If the current point is in the soft_wrap, skip comparing it
|
||||
if point.column() < start_column {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(boundary) = boundary {
|
||||
return boundary;
|
||||
} else if point.row() == 0 {
|
||||
return DisplayPoint::zero();
|
||||
} else {
|
||||
*point.row_mut() -= 1;
|
||||
if let Some((prev_ch, prev_point)) = prev {
|
||||
if is_boundary(ch, prev_ch) {
|
||||
return prev_point;
|
||||
}
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
}
|
||||
DisplayPoint::zero()
|
||||
}
|
||||
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line. If no boundary is found, the start of the line is returned.
|
||||
pub fn find_preceding_boundary_in_line(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut start_column = 0;
|
||||
if from.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
|
||||
start_column = indent;
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.reverse_chars_at(from) {
|
||||
if let Some((prev_ch, prev_point)) = prev {
|
||||
if is_boundary(ch, prev_ch) {
|
||||
return prev_point;
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' || point.column() < start_column {
|
||||
break;
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
}
|
||||
|
||||
prev.map(|(_, point)| point).unwrap_or(from)
|
||||
}
|
||||
|
||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||
@ -223,26 +264,48 @@ pub fn find_preceding_boundary(
|
||||
/// or end of a line.
|
||||
pub fn find_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut prev_ch = None;
|
||||
for ch in map.chars_at(point) {
|
||||
for (ch, point) in map.chars_at(from) {
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
break;
|
||||
return map.clip_point(point, Bias::Right);
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
prev_ch = Some(ch);
|
||||
}
|
||||
map.clip_point(point, Bias::Right)
|
||||
map.clip_point(map.max_point(), Bias::Right)
|
||||
}
|
||||
|
||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line. If no boundary is found, the end of the line is returned
|
||||
pub fn find_boundary_in_line(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.chars_at(from) {
|
||||
if let Some((prev_ch, _)) = prev {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
return map.clip_point(point, Bias::Right);
|
||||
}
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the last position checked so that we give a point right before the newline or eof.
|
||||
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
|
||||
}
|
||||
|
||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
|
@ -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" }
|
||||
@ -15,14 +28,12 @@ editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
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"
|
||||
|
||||
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_contexts::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
|
@ -206,7 +206,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
|
||||
@ -239,12 +239,12 @@ fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
fn next_word_start(
|
||||
pub(crate) fn next_word_start(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
@ -255,7 +255,7 @@ fn next_word_start(
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
||||
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
@ -272,23 +272,28 @@ fn next_word_end(
|
||||
ignore_punctuation: bool,
|
||||
) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
dbg!(point);
|
||||
point = movement::find_boundary(map, point, |left, right| {
|
||||
dbg!(left);
|
||||
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()
|
||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||
});
|
||||
|
||||
dbg!(point);
|
||||
|
||||
// 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')
|
||||
.map(|(c, _)| c == '\n')
|
||||
.unwrap_or(true)
|
||||
{
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
}
|
||||
map.clip_point(point, Bias::Left)
|
||||
dbg!(map.clip_point(point, Bias::Left))
|
||||
}
|
||||
|
||||
fn previous_word_start(
|
||||
@ -307,22 +312,21 @@ fn previous_word_start(
|
||||
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 {
|
||||
|
@ -6,6 +6,7 @@ use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
};
|
||||
@ -16,7 +17,11 @@ use gpui::{actions, MutableAppContext, ViewContext};
|
||||
use language::{AutoindentMode, Point, SelectionGoal};
|
||||
use workspace::Workspace;
|
||||
|
||||
use self::{change::change_over, delete::delete_over, yank::yank_over};
|
||||
use self::{
|
||||
change::{change_motion, change_object},
|
||||
delete::{delete_motion, delete_object},
|
||||
yank::{yank_motion, yank_object},
|
||||
};
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
@ -43,22 +48,22 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
delete_over(vim, Motion::Left, cx);
|
||||
delete_motion(vim, Motion::Left, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
delete_over(vim, Motion::Right, cx);
|
||||
delete_motion(vim, Motion::Right, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
change_over(vim, Motion::EndOfLine, cx);
|
||||
change_motion(vim, Motion::EndOfLine, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
delete_over(vim, Motion::EndOfLine, cx);
|
||||
delete_motion(vim, Motion::EndOfLine, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(paste);
|
||||
@ -70,17 +75,36 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
match vim.state.operator_stack.pop() {
|
||||
None => move_cursor(vim, motion, cx),
|
||||
Some(Operator::Namespace(_)) => {
|
||||
// Can't do anything for a namespace operator. Ignoring
|
||||
Some(Operator::Change) => change_motion(vim, motion, cx),
|
||||
Some(Operator::Delete) => delete_motion(vim, motion, cx),
|
||||
Some(Operator::Yank) => yank_motion(vim, motion, cx),
|
||||
_ => {
|
||||
// Can't do anything for text objects or namespace operators. Ignoring
|
||||
}
|
||||
Some(Operator::Change) => change_over(vim, motion, cx),
|
||||
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
||||
Some(Operator::Yank) => yank_over(vim, motion, cx),
|
||||
}
|
||||
vim.clear_operator(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
match vim.state.operator_stack.pop() {
|
||||
Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
|
||||
Some(Operator::Change) => change_object(vim, object, around, cx),
|
||||
Some(Operator::Delete) => delete_object(vim, object, around, cx),
|
||||
Some(Operator::Yank) => yank_object(vim, object, around, cx),
|
||||
_ => {
|
||||
// Can't do anything for namespace operators. Ignoring
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
// Can't do anything with change/delete/yank and text objects. Ignoring
|
||||
}
|
||||
}
|
||||
vim.clear_operator(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
@ -304,7 +328,7 @@ mod test {
|
||||
Mode::{self, *},
|
||||
Namespace, Operator,
|
||||
},
|
||||
vim_test_context::VimTestContext,
|
||||
test_contexts::VimTestContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
||||
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
|
||||
use editor::{char_kind, movement, Autoscroll};
|
||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
||||
use serde::Deserialize;
|
||||
@ -17,7 +17,7 @@ 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, 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
|
||||
@ -34,6 +34,23 @@ 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) {
|
||||
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| {
|
||||
object.expand_selection(map, selection, around);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, 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
|
||||
@ -78,7 +95,7 @@ fn change_word(
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{state::Mode, test_contexts::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
@ -170,8 +187,7 @@ mod test {
|
||||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ˇ"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-e"]);
|
||||
@ -193,6 +209,7 @@ mod test {
|
||||
Test ˇ
|
||||
test"},
|
||||
);
|
||||
println!("Marker");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
@ -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, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, 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_contexts::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, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
488
crates/vim/src/object.rs
Normal file
488
crates/vim/src/object.rs
Normal file
@ -0,0 +1,488 @@
|
||||
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, normal::normal_object, state::Mode, visual::visual_object, Vim};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum Object {
|
||||
Word { ignore_punctuation: bool },
|
||||
Sentence,
|
||||
Paragraph,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Word {
|
||||
#[serde(default)]
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
actions!(vim, [Sentence, Paragraph]);
|
||||
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, _: &Paragraph, cx: _| object(Object::Paragraph, 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 object_range(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
around: bool,
|
||||
) -> 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),
|
||||
_ => relative_to..relative_to,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_selection(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
) {
|
||||
let range = self.object_range(map, selection.head(), around);
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) -> 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,
|
||||
motion::right(map, relative_to),
|
||||
|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)
|
||||
});
|
||||
|
||||
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,
|
||||
) -> 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,
|
||||
) -> Range<DisplayPoint> {
|
||||
expand_to_include_whitespace(map, in_word(map, relative_to, ignore_punctuation), true)
|
||||
}
|
||||
|
||||
fn around_next_word(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
) -> Range<DisplayPoint> {
|
||||
// Get the start of the word
|
||||
let start = movement::find_preceding_boundary_in_line(
|
||||
map,
|
||||
motion::right(map, relative_to),
|
||||
|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
|
||||
});
|
||||
|
||||
start..end
|
||||
}
|
||||
|
||||
// /// Return the range containing a sentence.
|
||||
// fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> Range<DisplayPoint> {
|
||||
// let mut previous_end = relative_to;
|
||||
// let mut start = None;
|
||||
|
||||
// // Seek backwards to find a period or double newline. Record the last non whitespace character as the
|
||||
// // possible start of the sentence. Alternatively if two newlines are found right after each other, return that.
|
||||
// let mut rev_chars = map.reverse_chars_at(relative_to).peekable();
|
||||
// while let Some((char, point)) = rev_chars.next() {
|
||||
// dbg!(char, point);
|
||||
// if char == '.' {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if char == '\n'
|
||||
// && (rev_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) || start.is_none())
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if !char.is_whitespace() {
|
||||
// start = Some(point);
|
||||
// }
|
||||
|
||||
// previous_end = point;
|
||||
// }
|
||||
|
||||
// let mut end = relative_to;
|
||||
// let mut chars = map.chars_at(relative_to).peekable();
|
||||
// while let Some((char, point)) = chars.next() {
|
||||
// if !char.is_whitespace() {
|
||||
// if start.is_none() {
|
||||
// start = Some(point);
|
||||
// }
|
||||
|
||||
// // Set the end to the point after the current non whitespace character
|
||||
// end = point;
|
||||
// *end.column_mut() += char.len_utf8() as u32;
|
||||
// }
|
||||
|
||||
// if char == '.' {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if char == '\n' {
|
||||
// if start.is_none() {
|
||||
// if let Some((_, next_point)) = chars.peek() {
|
||||
// end = *next_point;
|
||||
// }
|
||||
// break;
|
||||
|
||||
// if chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// start.unwrap_or(previous_end)..end
|
||||
// }
|
||||
|
||||
fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> Range<DisplayPoint> {
|
||||
let mut start = None;
|
||||
let mut previous_end = relative_to;
|
||||
|
||||
for (char, point) in 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;
|
||||
}
|
||||
|
||||
// Handle case where cursor was before the sentence start
|
||||
let mut chars = map.chars_at(relative_to).peekable();
|
||||
if start.is_none() {
|
||||
if let Some((char, point)) = chars.peek() {
|
||||
if is_possible_sentence_start(*char) {
|
||||
start = Some(*point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut end = relative_to;
|
||||
for (char, point) in chars {
|
||||
if start.is_some() {
|
||||
if !char.is_whitespace() {
|
||||
end = point;
|
||||
*end.column_mut() += char.len_utf8() as u32;
|
||||
end = map.clip_point(end, Bias::Left);
|
||||
}
|
||||
|
||||
if is_sentence_end(map, point) {
|
||||
break;
|
||||
}
|
||||
} else if is_possible_sentence_start(char) {
|
||||
if around {
|
||||
start = Some(point);
|
||||
} else {
|
||||
end = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut range = start.unwrap_or(previous_end)..end;
|
||||
if around {
|
||||
range = expand_to_include_whitespace(map, range, false);
|
||||
}
|
||||
|
||||
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 chars = map.chars_at(point).peekable();
|
||||
|
||||
if let Some((char, _)) = chars.next() {
|
||||
if char == '\n' && chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !SENTENCE_END_PUNCTUATION.contains(&char) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char, _) in chars {
|
||||
if SENTENCE_END_WHITESPACE.contains(&char) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !SENTENCE_END_FILLERS.contains(&char) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
for (char, point) in map.chars_at(range.end) {
|
||||
range.end = point;
|
||||
|
||||
if char == '\n' && stop_at_newline {
|
||||
break;
|
||||
}
|
||||
|
||||
if char.is_whitespace() {
|
||||
whitespace_included = true;
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test_contexts::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_in_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_change_in_word", cx)
|
||||
.await
|
||||
.binding(["c", "i", "w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
let mut cx = cx.consume().binding(["c", "i", "shift-w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_in_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_delete_in_word", cx)
|
||||
.await
|
||||
.binding(["d", "i", "w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
let mut cx = cx.consume().binding(["d", "i", "shift-w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_around_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_change_around_word", cx)
|
||||
.await
|
||||
.binding(["c", "a", "w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
let mut cx = cx.consume().binding(["c", "a", "shift-w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_around_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_delete_around_word", cx)
|
||||
.await
|
||||
.binding(["d", "a", "w"]);
|
||||
cx.assert_all(WORD_LOCATIONS).await;
|
||||
let mut cx = cx.consume().binding(["d", "a", "shift-w"]);
|
||||
cx.assert_all(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
|
||||
"},
|
||||
// Double newlines are broken currently
|
||||
// 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_in_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_change_in_sentence", cx)
|
||||
.await
|
||||
.binding(["c", "i", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_in_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_delete_in_sentence", cx)
|
||||
.await
|
||||
.binding(["d", "i", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[ignore] // End cursor position is incorrect
|
||||
async fn test_change_around_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_change_around_sentence", cx)
|
||||
.await
|
||||
.binding(["c", "a", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[ignore] // End cursor position is incorrect
|
||||
async fn test_delete_around_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_delete_around_sentence", cx)
|
||||
.await
|
||||
.binding(["d", "a", "s"]);
|
||||
for sentence_example in SENTENCE_EXAMPLES {
|
||||
cx.assert_all(sentence_example).await;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use editor::CursorShape;
|
||||
use gpui::keymap::Context;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
@ -26,6 +26,7 @@ pub enum Operator {
|
||||
Change,
|
||||
Delete,
|
||||
Yank,
|
||||
Object { around: bool },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -77,7 +78,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
|
||||
}
|
||||
@ -87,6 +93,8 @@ impl Operator {
|
||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
||||
let operator_context = match operator {
|
||||
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",
|
||||
|
9
crates/vim/src/test_contexts.rs
Normal file
9
crates/vim/src/test_contexts.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod neovim_backed_binding_test_context;
|
||||
mod neovim_backed_test_context;
|
||||
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::*;
|
@ -0,0 +1,56 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
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 async fn assert(&mut self, initial_state: &str) {
|
||||
self.cx
|
||||
.assert_binding_matches(self.keystrokes_under_test, initial_state)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn assert_all(&mut self, 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(&marked_text).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
|
||||
}
|
||||
}
|
374
crates/vim/src/test_contexts/neovim_backed_test_context.rs
Normal file
374
crates/vim/src/test_contexts/neovim_backed_test_context.rs
Normal file
@ -0,0 +1,374 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use editor::DisplayPoint;
|
||||
use gpui::keymap::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
use super::{NeovimBackedBindingTestContext, VimTestContext};
|
||||
|
||||
pub struct NeovimBackedTestContext<'a> {
|
||||
cx: VimTestContext<'a>,
|
||||
test_case_id: &'static str,
|
||||
data_counter: usize,
|
||||
#[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<'a> NeovimBackedTestContext<'a> {
|
||||
pub async fn new(
|
||||
test_case_id: &'static str,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> NeovimBackedTestContext<'a> {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
let handler = NvimHandler {};
|
||||
#[cfg(feature = "neovim")]
|
||||
let (nvim, join_handle, child) = Compat::new(async {
|
||||
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");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
let result = Self {
|
||||
cx,
|
||||
test_case_id,
|
||||
data_counter: 0,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
result.clear_test_data()
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
let window_id = self.window_id;
|
||||
self.cx.dispatch_keystroke(window_id, keystroke, false);
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_shared_keystroke(keystroke_text).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) {
|
||||
self.set_state(marked_text, Mode::Normal);
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
let cursor_point =
|
||||
self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let mut lines = self
|
||||
.buffer_text()
|
||||
.lines()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if lines.len() > 1 {
|
||||
// Add final newline which is missing from buffer_text
|
||||
lines.push("".to_string());
|
||||
}
|
||||
|
||||
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");
|
||||
nvim_window
|
||||
.set_cursor((
|
||||
cursor_point.head().row as i64 + 1,
|
||||
cursor_point.head().column as i64,
|
||||
))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
assert_eq!(self.neovim_text().await, self.buffer_text());
|
||||
|
||||
let zed_head = self.update_editor(|editor, cx| editor.selections.newest_display(cx).head());
|
||||
assert_eq!(self.neovim_head().await, zed_head);
|
||||
|
||||
if let Some(neovim_mode) = self.neovim_mode().await {
|
||||
assert_eq!(neovim_mode, self.mode());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn neovim_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.write_test_data(text.clone(), "text");
|
||||
text
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn neovim_text(&mut self) -> String {
|
||||
self.read_test_data("text")
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn neovim_head(&mut self) -> DisplayPoint {
|
||||
let nvim_row: u32 = self
|
||||
.nvim
|
||||
.command_output("echo line('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim rows start at 1
|
||||
let nvim_column: u32 = self
|
||||
.nvim
|
||||
.command_output("echo col('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim columns start at 1
|
||||
|
||||
let serialized = format!("{},{}", nvim_row.to_string(), nvim_column.to_string());
|
||||
self.write_test_data(serialized, "head");
|
||||
|
||||
DisplayPoint::new(nvim_row, nvim_column)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn neovim_head(&mut self) -> DisplayPoint {
|
||||
let serialized = self.read_test_data("head");
|
||||
let mut components = serialized.split(',');
|
||||
let nvim_row = components.next().unwrap().parse::<u32>().unwrap();
|
||||
let nvim_column = components.next().unwrap().parse::<u32>().unwrap();
|
||||
|
||||
DisplayPoint::new(nvim_row, nvim_column)
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn neovim_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,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&mode).expect("Could not serialize mode");
|
||||
|
||||
self.write_test_data(serialized, "mode");
|
||||
|
||||
mode
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn neovim_mode(&mut self) -> Option<Mode> {
|
||||
let serialized = self.read_test_data("mode");
|
||||
serde_json::from_str(&serialized).expect("Could not deserialize test data")
|
||||
}
|
||||
|
||||
fn test_data_directory(&self) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(self.test_case_id);
|
||||
data_path
|
||||
}
|
||||
|
||||
fn next_data_path(&mut self, kind: &str) -> PathBuf {
|
||||
let mut data_path = self.test_data_directory();
|
||||
data_path.push(format!("{}{}.txt", self.data_counter, kind));
|
||||
self.data_counter += 1;
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(&mut self, kind: &str) -> String {
|
||||
let path = self.next_data_path(kind);
|
||||
std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn write_test_data(&mut self, data: String, kind: &str) {
|
||||
let path = self.next_data_path(kind);
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("Could not create test data directory");
|
||||
std::fs::write(path, data).expect("Could not write out test data");
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn clear_test_data(&self) {
|
||||
// If the path does not exist, no biggy, we will create it
|
||||
std::fs::remove_dir_all(self.test_data_directory()).ok();
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
) {
|
||||
dbg!(keystrokes, initial_state);
|
||||
self.set_shared_state(initial_state).await;
|
||||
self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_state_matches().await;
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
NeovimBackedBindingTestContext::new(keystrokes, self)
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
69
crates/vim/src/test_contexts/vim_binding_test_context.rs
Normal file
69
crates/vim/src/test_contexts/vim_binding_test_context.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::*;
|
||||
|
||||
use super::VimTestContext;
|
||||
|
||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: VimTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
cx: VimTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
mode_before,
|
||||
mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||
VimBindingTestContext {
|
||||
keystrokes_under_test,
|
||||
cx: self.cx,
|
||||
mode_before: self.mode_before,
|
||||
mode_after: self.mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||
self.mode_after = mode_after;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
self.mode_before,
|
||||
state_after,
|
||||
self.mode_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ use workspace::{pane, AppState, WorkspaceHandle};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
use super::VimBindingTestContext;
|
||||
|
||||
pub struct VimTestContext<'a> {
|
||||
cx: EditorTestContext<'a>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
@ -168,67 +170,3 @@ impl<'a> DerefMut for VimTestContext<'a> {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: VimTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
mode_before: Mode,
|
||||
mode_after: Mode,
|
||||
cx: VimTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
mode_before,
|
||||
mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||
VimBindingTestContext {
|
||||
keystrokes_under_test,
|
||||
cx: self.cx,
|
||||
mode_before: self.mode_before,
|
||||
mode_after: self.mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||
self.mode_after = mode_after;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
self.mode_before,
|
||||
state_after,
|
||||
self.mode_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
#[cfg(test)]
|
||||
mod vim_test_context;
|
||||
mod test_contexts;
|
||||
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
mod motion;
|
||||
mod normal;
|
||||
mod object;
|
||||
mod state;
|
||||
mod utils;
|
||||
mod visual;
|
||||
@ -32,6 +33,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
normal::init(cx);
|
||||
visual::init(cx);
|
||||
insert::init(cx);
|
||||
object::init(cx);
|
||||
motion::init(cx);
|
||||
|
||||
// Vim Actions
|
||||
@ -144,7 +146,8 @@ impl Vim {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -210,7 +213,10 @@ mod test {
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
@ -219,6 +225,19 @@ mod test {
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new("test_neovim", cx).await;
|
||||
|
||||
cx.simulate_shared_keystroke("i").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;
|
||||
|
@ -6,7 +6,7 @@ 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, utils::copy_selections_content, Vim};
|
||||
|
||||
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
|
||||
|
||||
@ -43,6 +43,8 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn visual_object(_object: Object, _cx: &mut MutableAppContext) {}
|
||||
|
||||
pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
@ -274,7 +276,7 @@ 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_contexts::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
||||
|
@ -0,0 +1 @@
|
||||
Fox Jumps! Over the lazy.
|
@ -0,0 +1 @@
|
||||
0,16
|
@ -0,0 +1 @@
|
||||
0,0
|
@ -0,0 +1 @@
|
||||
"Insert"
|
@ -0,0 +1 @@
|
||||
Fox Jumps! Over the lazy.
|
@ -0,0 +1 @@
|
||||
0,0
|
@ -0,0 +1 @@
|
||||
"Insert"
|
@ -0,0 +1 @@
|
||||
Fox Jumps! Over the lazy.
|
@ -0,0 +1 @@
|
||||
0,0
|
@ -0,0 +1 @@
|
||||
"Insert"
|
@ -0,0 +1 @@
|
||||
The quick brown? Over the lazy.
|
12
crates/vim/test_data/test_change_around_word/0text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/0text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/100head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/100head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,0
|
1
crates/vim/test_data/test_change_around_word/101mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/101mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/102text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/102text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/103head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/103head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,0
|
1
crates/vim/test_data/test_change_around_word/104mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/104mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/105text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/105text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/106head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/106head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,0
|
1
crates/vim/test_data/test_change_around_word/107mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/107mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/108text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/108text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/109head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/109head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,0
|
1
crates/vim/test_data/test_change_around_word/10head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/10head.txt
Normal file
@ -0,0 +1 @@
|
||||
1,4
|
1
crates/vim/test_data/test_change_around_word/110mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/110mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/111text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/111text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/112head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/112head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,9
|
1
crates/vim/test_data/test_change_around_word/113mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/113mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/114text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/114text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/115head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/115head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,10
|
1
crates/vim/test_data/test_change_around_word/116mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/116mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
9
crates/vim/test_data/test_change_around_word/117text.txt
Normal file
9
crates/vim/test_data/test_change_around_word/117text.txt
Normal file
@ -0,0 +1,9 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/118head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/118head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,15
|
1
crates/vim/test_data/test_change_around_word/119mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/119mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
1
crates/vim/test_data/test_change_around_word/11mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/11mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
10
crates/vim/test_data/test_change_around_word/120text.txt
Normal file
10
crates/vim/test_data/test_change_around_word/120text.txt
Normal file
@ -0,0 +1,10 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/121head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/121head.txt
Normal file
@ -0,0 +1 @@
|
||||
7,0
|
1
crates/vim/test_data/test_change_around_word/122mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/122mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/123text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/123text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/124head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/124head.txt
Normal file
@ -0,0 +1 @@
|
||||
8,0
|
1
crates/vim/test_data/test_change_around_word/125mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/125mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/126text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/126text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/127head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/127head.txt
Normal file
@ -0,0 +1 @@
|
||||
9,0
|
1
crates/vim/test_data/test_change_around_word/128mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/128mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/129text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/129text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
over
|
||||
the lazy dog
|
||||
|
12
crates/vim/test_data/test_change_around_word/12text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/12text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/130head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/130head.txt
Normal file
@ -0,0 +1 @@
|
||||
9,2
|
1
crates/vim/test_data/test_change_around_word/131mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/131mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/132text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/132text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
1
crates/vim/test_data/test_change_around_word/133head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/133head.txt
Normal file
@ -0,0 +1 @@
|
||||
10,12
|
1
crates/vim/test_data/test_change_around_word/134mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/134mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/135text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/135text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
1
crates/vim/test_data/test_change_around_word/136head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/136head.txt
Normal file
@ -0,0 +1 @@
|
||||
11,0
|
1
crates/vim/test_data/test_change_around_word/137mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/137mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
1
crates/vim/test_data/test_change_around_word/13head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/13head.txt
Normal file
@ -0,0 +1 @@
|
||||
1,4
|
1
crates/vim/test_data/test_change_around_word/14mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/14mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/15text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/15text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/16head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/16head.txt
Normal file
@ -0,0 +1 @@
|
||||
1,9
|
1
crates/vim/test_data/test_change_around_word/17mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/17mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/18text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/18text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/19head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/19head.txt
Normal file
@ -0,0 +1 @@
|
||||
2,12
|
1
crates/vim/test_data/test_change_around_word/1head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/1head.txt
Normal file
@ -0,0 +1 @@
|
||||
0,10
|
1
crates/vim/test_data/test_change_around_word/20mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/20mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/21text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/21text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/22head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/22head.txt
Normal file
@ -0,0 +1 @@
|
||||
3,0
|
1
crates/vim/test_data/test_change_around_word/23mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/23mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/24text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/24text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
The-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/25head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/25head.txt
Normal file
@ -0,0 +1 @@
|
||||
4,0
|
1
crates/vim/test_data/test_change_around_word/26mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/26mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
11
crates/vim/test_data/test_change_around_word/27text.txt
Normal file
11
crates/vim/test_data/test_change_around_word/27text.txt
Normal file
@ -0,0 +1,11 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/28head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/28head.txt
Normal file
@ -0,0 +1 @@
|
||||
5,0
|
1
crates/vim/test_data/test_change_around_word/29mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/29mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
1
crates/vim/test_data/test_change_around_word/2mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/2mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/30text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/30text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
-quick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/31head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/31head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,0
|
1
crates/vim/test_data/test_change_around_word/32mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/32mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
12
crates/vim/test_data/test_change_around_word/33text.txt
Normal file
12
crates/vim/test_data/test_change_around_word/33text.txt
Normal file
@ -0,0 +1,12 @@
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
|
||||
|
||||
|
||||
Thequick brown
|
||||
|
||||
|
||||
fox-jumps over
|
||||
the lazy dog
|
||||
|
1
crates/vim/test_data/test_change_around_word/34head.txt
Normal file
1
crates/vim/test_data/test_change_around_word/34head.txt
Normal file
@ -0,0 +1 @@
|
||||
6,3
|
1
crates/vim/test_data/test_change_around_word/35mode.txt
Normal file
1
crates/vim/test_data/test_change_around_word/35mode.txt
Normal file
@ -0,0 +1 @@
|
||||
"Insert"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user