Fixed some neovim test context issues, added repeated commands in vim mode, and ported some tests to use the neovim testing strategy
This commit is contained in:
parent
b82db3a254
commit
515c1ea123
971 changed files with 838 additions and 11898 deletions
7629
Cargo.lock
generated
7629
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,11 +9,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"h": "vim::Left",
|
"h": "vim::Left",
|
||||||
"backspace": "vim::Left",
|
"backspace": "vim::Backspace",
|
||||||
"j": "vim::Down",
|
"j": "vim::Down",
|
||||||
"k": "vim::Up",
|
"k": "vim::Up",
|
||||||
"l": "vim::Right",
|
"l": "vim::Right",
|
||||||
"0": "vim::StartOfLine",
|
|
||||||
"$": "vim::EndOfLine",
|
"$": "vim::EndOfLine",
|
||||||
"shift-g": "vim::EndOfDocument",
|
"shift-g": "vim::EndOfDocument",
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
|
@ -54,6 +53,43 @@
|
||||||
"around": true
|
"around": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
|
"1": [
|
||||||
|
"vim::Number",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
"vim::Number",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
"vim::Number",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"4": [
|
||||||
|
"vim::Number",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
"vim::Number",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"6": [
|
||||||
|
"vim::Number",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"7": [
|
||||||
|
"vim::Number",
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"8": [
|
||||||
|
"vim::Number",
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"9": [
|
||||||
|
"vim::Number",
|
||||||
|
9
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -114,6 +150,15 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == n",
|
||||||
|
"bindings": {
|
||||||
|
"0": [
|
||||||
|
"vim::Number",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == g",
|
"context": "Editor && vim_operator == g",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -128,13 +173,6 @@
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == c",
|
"context": "Editor && vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"w": "vim::ChangeWord",
|
|
||||||
"shift-w": [
|
|
||||||
"vim::ChangeWord",
|
|
||||||
{
|
|
||||||
"ignorePunctuation": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -160,8 +198,7 @@
|
||||||
"ignorePunctuation": true
|
"ignorePunctuation": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"s": "vim::Sentence",
|
"s": "vim::Sentence"
|
||||||
"p": "vim::Paragraph"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -4961,6 +4961,7 @@ async fn test_random_collaboration(
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
next_entity_id,
|
next_entity_id,
|
||||||
|
cx.function_name.clone(),
|
||||||
);
|
);
|
||||||
let host = server.create_client(&mut host_cx, "host").await;
|
let host = server.create_client(&mut host_cx, "host").await;
|
||||||
let host_project = host_cx.update(|cx| {
|
let host_project = host_cx.update(|cx| {
|
||||||
|
@ -5194,6 +5195,7 @@ async fn test_random_collaboration(
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
next_entity_id,
|
next_entity_id,
|
||||||
|
cx.function_name.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
deterministic.start_waiting();
|
deterministic.start_waiting();
|
||||||
|
|
|
@ -4,19 +4,25 @@ use crate::{
|
||||||
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use collections::BTreeMap;
|
||||||
use futures::{Future, StreamExt};
|
use futures::{Future, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
use itertools::Itertools;
|
||||||
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
|
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
|
||||||
use lsp::{notification, request};
|
use lsp::{notification, request};
|
||||||
|
use parking_lot::RwLock;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
ops::{Deref, DerefMut, Range},
|
ops::{Deref, DerefMut, Range},
|
||||||
sync::Arc,
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use util::{
|
use util::{
|
||||||
assert_set_eq, set_eq,
|
assert_set_eq, set_eq,
|
||||||
|
@ -85,6 +91,7 @@ pub struct EditorTestContext<'a> {
|
||||||
pub cx: &'a mut gpui::TestAppContext,
|
pub cx: &'a mut gpui::TestAppContext,
|
||||||
pub window_id: usize,
|
pub window_id: usize,
|
||||||
pub editor: ViewHandle<Editor>,
|
pub editor: ViewHandle<Editor>,
|
||||||
|
pub assertion_context: AssertionContextManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EditorTestContext<'a> {
|
impl<'a> EditorTestContext<'a> {
|
||||||
|
@ -106,9 +113,14 @@ impl<'a> EditorTestContext<'a> {
|
||||||
cx,
|
cx,
|
||||||
window_id,
|
window_id,
|
||||||
editor,
|
editor,
|
||||||
|
assertion_context: AssertionContextManager::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
|
||||||
|
self.assertion_context.add_context(context)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn condition(
|
pub fn condition(
|
||||||
&self,
|
&self,
|
||||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||||
|
@ -394,6 +406,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
cx,
|
cx,
|
||||||
window_id,
|
window_id,
|
||||||
editor,
|
editor,
|
||||||
|
assertion_context: AssertionContextManager::new(),
|
||||||
},
|
},
|
||||||
lsp,
|
lsp,
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -507,3 +520,45 @@ impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||||
&mut self.cx
|
&mut self.cx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssertionContextManager {
|
||||||
|
id: Arc<AtomicUsize>,
|
||||||
|
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssertionContextManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Arc::new(AtomicUsize::new(0)),
|
||||||
|
contexts: Arc::new(RwLock::new(BTreeMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_context(&self, context: String) -> ContextHandle {
|
||||||
|
let id = self.id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let mut contexts = self.contexts.write();
|
||||||
|
contexts.insert(id, context);
|
||||||
|
ContextHandle {
|
||||||
|
id,
|
||||||
|
manager: self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> String {
|
||||||
|
let contexts = self.contexts.read();
|
||||||
|
format!("\n{}\n", contexts.values().join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContextHandle {
|
||||||
|
id: usize,
|
||||||
|
manager: AssertionContextManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ContextHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut contexts = self.manager.contexts.write();
|
||||||
|
contexts.remove(&self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -182,6 +182,7 @@ pub struct TestAppContext {
|
||||||
cx: Rc<RefCell<MutableAppContext>>,
|
cx: Rc<RefCell<MutableAppContext>>,
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
condition_duration: Option<Duration>,
|
condition_duration: Option<Duration>,
|
||||||
|
pub function_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WindowInputHandler {
|
pub struct WindowInputHandler {
|
||||||
|
@ -437,6 +438,7 @@ impl TestAppContext {
|
||||||
font_cache: Arc<FontCache>,
|
font_cache: Arc<FontCache>,
|
||||||
leak_detector: Arc<Mutex<LeakDetector>>,
|
leak_detector: Arc<Mutex<LeakDetector>>,
|
||||||
first_entity_id: usize,
|
first_entity_id: usize,
|
||||||
|
function_name: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut cx = MutableAppContext::new(
|
let mut cx = MutableAppContext::new(
|
||||||
foreground,
|
foreground,
|
||||||
|
@ -456,6 +458,7 @@ impl TestAppContext {
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
cx: Rc::new(RefCell::new(cx)),
|
||||||
foreground_platform,
|
foreground_platform,
|
||||||
condition_duration: None,
|
condition_duration: None,
|
||||||
|
function_name,
|
||||||
};
|
};
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -37,6 +37,7 @@ pub fn run_test(
|
||||||
u64,
|
u64,
|
||||||
bool,
|
bool,
|
||||||
)),
|
)),
|
||||||
|
fn_name: String,
|
||||||
) {
|
) {
|
||||||
// let _profiler = dhat::Profiler::new_heap();
|
// let _profiler = dhat::Profiler::new_heap();
|
||||||
|
|
||||||
|
@ -78,6 +79,7 @@ pub fn run_test(
|
||||||
font_cache.clone(),
|
font_cache.clone(),
|
||||||
leak_detector.clone(),
|
leak_detector.clone(),
|
||||||
0,
|
0,
|
||||||
|
fn_name.clone(),
|
||||||
);
|
);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
test_fn(
|
test_fn(
|
||||||
|
|
|
@ -117,6 +117,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
cx.font_cache().clone(),
|
cx.font_cache().clone(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
#first_entity_id,
|
#first_entity_id,
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
));
|
));
|
||||||
cx_teardowns.extend(quote!(
|
cx_teardowns.extend(quote!(
|
||||||
|
@ -149,7 +150,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
#cx_vars
|
#cx_vars
|
||||||
cx.foreground().run(#inner_fn_name(#inner_fn_args));
|
cx.foreground().run(#inner_fn_name(#inner_fn_args));
|
||||||
#cx_teardowns
|
#cx_teardowns
|
||||||
}
|
},
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +189,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
#num_iterations as u64,
|
#num_iterations as u64,
|
||||||
#starting_seed as u64,
|
#starting_seed as u64,
|
||||||
#max_retries,
|
#max_retries,
|
||||||
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args)
|
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args),
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ workspace = { path = "../workspace" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "1.0.4"
|
indoc = "1.0.4"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
|
|
@ -18,6 +18,7 @@ use crate::{
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum Motion {
|
pub enum Motion {
|
||||||
Left,
|
Left,
|
||||||
|
Backspace,
|
||||||
Down,
|
Down,
|
||||||
Up,
|
Up,
|
||||||
Right,
|
Right,
|
||||||
|
@ -58,6 +59,7 @@ actions!(
|
||||||
vim,
|
vim,
|
||||||
[
|
[
|
||||||
Left,
|
Left,
|
||||||
|
Backspace,
|
||||||
Down,
|
Down,
|
||||||
Up,
|
Up,
|
||||||
Right,
|
Right,
|
||||||
|
@ -74,6 +76,7 @@ impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
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, _: &Down, cx: _| motion(Motion::Down, cx));
|
||||||
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
||||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, 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) {
|
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
|
||||||
if let Some(Operator::Namespace(_)) = vim.active_operator() {
|
Vim::update(cx, |vim, cx| vim.pop_operator(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 {
|
match Vim::read(cx).state.mode {
|
||||||
Mode::Normal => normal_motion(motion, cx),
|
Mode::Normal => normal_motion(motion, operator, times, cx),
|
||||||
Mode::Visual { .. } => visual_motion(motion, cx),
|
Mode::Visual { .. } => visual_motion(motion, times, cx),
|
||||||
Mode::Insert => {
|
Mode::Insert => {
|
||||||
// Shouldn't execute a motion in insert mode. Ignoring
|
// Shouldn't execute a motion in insert mode. Ignoring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Motion handling is specified here:
|
// Motion handling is specified here:
|
||||||
|
@ -154,6 +159,7 @@ impl Motion {
|
||||||
use Motion::*;
|
use Motion::*;
|
||||||
match self {
|
match self {
|
||||||
Left => (left(map, point), SelectionGoal::None),
|
Left => (left(map, point), SelectionGoal::None),
|
||||||
|
Backspace => (movement::left(map, point), SelectionGoal::None),
|
||||||
Down => movement::down(map, point, goal, true),
|
Down => movement::down(map, point, goal, true),
|
||||||
Up => movement::up(map, point, goal, true),
|
Up => movement::up(map, point, goal, true),
|
||||||
Right => (right(map, point), SelectionGoal::None),
|
Right => (right(map, point), SelectionGoal::None),
|
||||||
|
@ -184,10 +190,13 @@ impl Motion {
|
||||||
self,
|
self,
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
selection: &mut Selection<DisplayPoint>,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
|
times: usize,
|
||||||
expand_to_surrounding_newline: bool,
|
expand_to_surrounding_newline: bool,
|
||||||
) {
|
) {
|
||||||
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
for _ in 0..times {
|
||||||
selection.set_head(head, goal);
|
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
||||||
|
selection.set_head(head, goal);
|
||||||
|
}
|
||||||
|
|
||||||
if self.linewise() {
|
if self.linewise() {
|
||||||
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
||||||
|
@ -272,17 +281,13 @@ fn next_word_end(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
*point.column_mut() += 1;
|
*point.column_mut() += 1;
|
||||||
dbg!(point);
|
|
||||||
point = movement::find_boundary(map, point, |left, right| {
|
point = movement::find_boundary(map, point, |left, right| {
|
||||||
dbg!(left);
|
|
||||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
left_kind != right_kind && left_kind != CharKind::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
|
// 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
|
// we have backtraced already
|
||||||
if !map
|
if !map
|
||||||
|
@ -293,7 +298,7 @@ fn next_word_end(
|
||||||
{
|
{
|
||||||
*point.column_mut() = point.column().saturating_sub(1);
|
*point.column_mut() = point.column().saturating_sub(1);
|
||||||
}
|
}
|
||||||
dbg!(map.clip_point(point, Bias::Left))
|
map.clip_point(point, Bias::Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_word_start(
|
fn previous_word_start(
|
||||||
|
|
|
@ -10,7 +10,6 @@ use crate::{
|
||||||
state::{Mode, Operator},
|
state::{Mode, Operator},
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
use change::init as change_init;
|
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint};
|
use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint};
|
||||||
use gpui::{actions, MutableAppContext, ViewContext};
|
use gpui::{actions, MutableAppContext, ViewContext};
|
||||||
|
@ -48,41 +47,47 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(insert_line_below);
|
cx.add_action(insert_line_below);
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
delete_motion(vim, Motion::Left, cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
|
delete_motion(vim, Motion::Left, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
delete_motion(vim, Motion::Right, cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
|
delete_motion(vim, Motion::Right, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
change_motion(vim, Motion::EndOfLine, cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
|
change_motion(vim, Motion::EndOfLine, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
delete_motion(vim, Motion::EndOfLine, cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
|
delete_motion(vim, Motion::EndOfLine, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(paste);
|
cx.add_action(paste);
|
||||||
|
|
||||||
change_init(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
|
pub fn normal_motion(
|
||||||
|
motion: Motion,
|
||||||
|
operator: Option<Operator>,
|
||||||
|
times: usize,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
match vim.state.operator_stack.pop() {
|
match operator {
|
||||||
None => move_cursor(vim, motion, cx),
|
None => move_cursor(vim, motion, times, cx),
|
||||||
Some(Operator::Change) => change_motion(vim, motion, cx),
|
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Delete) => delete_motion(vim, motion, cx),
|
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Yank) => yank_motion(vim, motion, cx),
|
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
||||||
_ => {
|
_ => {
|
||||||
// Can't do anything for text objects or namespace operators. Ignoring
|
// Can't do anything for text objects or namespace operators. Ignoring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vim.clear_operator(cx);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,10 +110,16 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal))
|
s.move_cursors_with(|map, cursor, goal| {
|
||||||
|
let mut result = (cursor, goal);
|
||||||
|
for _ in 0..times {
|
||||||
|
result = motion.move_point(map, result.0, result.1);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -328,311 +339,139 @@ mod test {
|
||||||
Mode::{self, *},
|
Mode::{self, *},
|
||||||
Namespace, Operator,
|
Namespace, Operator,
|
||||||
},
|
},
|
||||||
test_contexts::VimTestContext,
|
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_h(cx: &mut gpui::TestAppContext) {
|
async fn test_h(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||||
let mut cx = cx.binding(["h"]);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert("The qˇuick", "The ˇquick");
|
ˇThe qˇuick
|
||||||
cx.assert("ˇThe quick", "ˇThe quick");
|
ˇbrown"
|
||||||
cx.assert(
|
})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick
|
|
||||||
ˇbrown"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇbrown"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
let mut cx = cx.binding(["backspace"]);
|
.await
|
||||||
cx.assert("The qˇuick", "The ˇquick");
|
.binding(["backspace"]);
|
||||||
cx.assert("ˇThe quick", "ˇThe quick");
|
cx.assert_all(indoc! {"
|
||||||
cx.assert(
|
ˇThe qˇuick
|
||||||
indoc! {"
|
ˇbrown"
|
||||||
The quick
|
})
|
||||||
ˇbrown"},
|
.await;
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇbrown"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_j(cx: &mut gpui::TestAppContext) {
|
async fn test_j(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
|
||||||
let mut cx = cx.binding(["j"]);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert(
|
ˇThe qˇuick broˇwn
|
||||||
indoc! {"
|
ˇfox jumps"
|
||||||
The ˇquick
|
})
|
||||||
brown fox"},
|
.await;
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
browˇn fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
browˇn fox"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
browˇn fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
brown"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
browˇn"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇbrown"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇbrown"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_k(cx: &mut gpui::TestAppContext) {
|
async fn test_k(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
|
||||||
let mut cx = cx.binding(["k"]);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert(
|
ˇThe qˇuick
|
||||||
indoc! {"
|
ˇbrown fˇox jumˇps"
|
||||||
The ˇquick
|
})
|
||||||
brown fox"},
|
.await;
|
||||||
indoc! {"
|
|
||||||
The ˇquick
|
|
||||||
brown fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
browˇn fox"},
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick
|
|
||||||
brown fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The
|
|
||||||
quicˇk"},
|
|
||||||
indoc! {"
|
|
||||||
Thˇe
|
|
||||||
quick"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_l(cx: &mut gpui::TestAppContext) {
|
async fn test_l(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
|
||||||
let mut cx = cx.binding(["l"]);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert("The qˇuick", "The quˇick");
|
ˇThe qˇuicˇk
|
||||||
cx.assert("The quicˇk", "The quicˇk");
|
ˇbrowˇn"})
|
||||||
cx.assert(
|
.await;
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
brown"},
|
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
brown"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
|
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
let mut cx = cx.binding(["$"]);
|
cx.assert_binding_matches_all(
|
||||||
cx.assert("Tˇest test", "Test tesˇt");
|
["$"],
|
||||||
cx.assert("Test tesˇt", "Test tesˇt");
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The ˇquick
|
ˇThe qˇuicˇk
|
||||||
brown"},
|
ˇbrowˇn"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
["0"],
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quicˇk
|
ˇThe qˇuicˇk
|
||||||
brown"},
|
ˇbrowˇn"},
|
||||||
);
|
)
|
||||||
cx.assert(
|
.await;
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
brown"},
|
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
brown"},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut cx = cx.binding(["0"]);
|
|
||||||
cx.assert("Test ˇtest", "ˇTest test");
|
|
||||||
cx.assert("ˇTest test", "ˇTest test");
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick
|
|
||||||
brown"},
|
|
||||||
indoc! {"
|
|
||||||
ˇThe quick
|
|
||||||
brown"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
ˇThe quick
|
|
||||||
brown"},
|
|
||||||
indoc! {"
|
|
||||||
ˇThe quick
|
|
||||||
brown"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
|
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
|
||||||
let mut cx = cx.binding(["shift-g"]);
|
|
||||||
|
|
||||||
cx.assert(
|
cx.assert_all(indoc! {"
|
||||||
indoc! {"
|
|
||||||
The ˇquick
|
The ˇquick
|
||||||
|
|
||||||
brown fox jumps
|
brown fox jumps
|
||||||
over the lazy dog"},
|
overˇ the lazy doˇg"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick
|
cx.assert(indoc! {"
|
||||||
|
|
||||||
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 quiˇck
|
The quiˇck
|
||||||
|
|
||||||
brown"},
|
brown"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick
|
cx.assert(indoc! {"
|
||||||
|
|
||||||
browˇn"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quiˇck
|
The quiˇck
|
||||||
|
|
||||||
"},
|
"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick
|
|
||||||
|
|
||||||
ˇ"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_w(cx: &mut gpui::TestAppContext) {
|
async fn test_w(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
|
||||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
cx.assert_all(indoc! {"
|
||||||
The ˇquickˇ-ˇbrown
|
The ˇquickˇ-ˇbrown
|
||||||
ˇ
|
ˇ
|
||||||
ˇ
|
ˇ
|
||||||
ˇfox_jumps ˇover
|
ˇfox_jumps ˇover
|
||||||
ˇthˇˇe"});
|
ˇthˇe"})
|
||||||
cx.set_state(
|
.await;
|
||||||
indoc! {"
|
let mut cx = cx.binding(["shift-w"]);
|
||||||
ˇThe quick-brown
|
cx.assert_all(indoc! {"
|
||||||
|
The ˇquickˇ-ˇbrown
|
||||||
|
|
||||||
fox_jumps over
|
|
||||||
the"},
|
|
||||||
Mode::Normal,
|
|
||||||
);
|
|
||||||
|
|
||||||
for cursor_offset in cursor_offsets {
|
|
||||||
cx.simulate_keystroke("w");
|
|
||||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset and test ignoring punctuation
|
|
||||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
|
||||||
The ˇquick-brown
|
|
||||||
ˇ
|
ˇ
|
||||||
ˇ
|
ˇ
|
||||||
ˇfox_jumps ˇover
|
ˇfox_jumps ˇover
|
||||||
ˇthˇˇe"});
|
ˇthˇe"})
|
||||||
cx.set_state(
|
.await;
|
||||||
indoc! {"
|
|
||||||
ˇThe quick-brown
|
|
||||||
|
|
||||||
|
|
||||||
fox_jumps over
|
|
||||||
the"},
|
|
||||||
Mode::Normal,
|
|
||||||
);
|
|
||||||
|
|
||||||
for cursor_offset in cursor_offsets {
|
|
||||||
cx.simulate_keystroke("shift-w");
|
|
||||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_e(cx: &mut gpui::TestAppContext) {
|
async fn test_e(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
|
||||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
cx.assert_all(indoc! {"
|
||||||
Thˇe quicˇkˇ-browˇn
|
Thˇe quicˇkˇ-browˇn
|
||||||
|
|
||||||
|
|
||||||
fox_jumpˇs oveˇr
|
fox_jumpˇs oveˇr
|
||||||
thˇe"});
|
thˇe"})
|
||||||
cx.set_state(
|
.await;
|
||||||
indoc! {"
|
let mut cx = cx.binding(["shift-e"]);
|
||||||
ˇThe quick-brown
|
cx.assert_all(indoc! {"
|
||||||
|
Thˇe quicˇkˇ-browˇn
|
||||||
|
|
||||||
fox_jumps over
|
|
||||||
the"},
|
|
||||||
Mode::Normal,
|
|
||||||
);
|
|
||||||
|
|
||||||
for cursor_offset in cursor_offsets {
|
|
||||||
cx.simulate_keystroke("e");
|
|
||||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset and test ignoring punctuation
|
|
||||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
|
||||||
Thˇe quick-browˇn
|
|
||||||
|
|
||||||
|
|
||||||
fox_jumpˇs oveˇr
|
fox_jumpˇs oveˇr
|
||||||
thˇˇe"});
|
thˇe"})
|
||||||
cx.set_state(
|
.await;
|
||||||
indoc! {"
|
|
||||||
ˇThe quick-brown
|
|
||||||
|
|
||||||
|
|
||||||
fox_jumps over
|
|
||||||
the"},
|
|
||||||
Mode::Normal,
|
|
||||||
);
|
|
||||||
for cursor_offset in cursor_offsets {
|
|
||||||
cx.simulate_keystroke("shift-e");
|
|
||||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -699,90 +538,35 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_gg(cx: &mut gpui::TestAppContext) {
|
async fn test_gg(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["g", "g"]);
|
||||||
let mut cx = cx.binding(["g", "g"]);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert(
|
The qˇuick
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
|
|
||||||
brown fox jumps
|
brown fox jumps
|
||||||
over ˇthe lazy dog"},
|
over ˇthe laˇzy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The qˇuick
|
cx.assert(indoc! {"
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The qˇuick
|
|
||||||
|
|
||||||
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 laˇzy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quicˇk
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
|
|
||||||
|
|
||||||
brown fox jumps
|
brown fox jumps
|
||||||
over the laˇzy dog"},
|
over the laˇzy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
ˇ
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_a(cx: &mut gpui::TestAppContext) {
|
async fn test_a(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
|
||||||
let mut cx = cx.binding(["a"]).mode_after(Mode::Insert);
|
cx.assert_all("The qˇuicˇk").await;
|
||||||
|
|
||||||
cx.assert("The qˇuick", "The quˇick");
|
|
||||||
cx.assert("The quicˇk", "The quickˇ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
|
async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
|
||||||
let mut cx = cx.binding(["shift-a"]).mode_after(Mode::Insert);
|
cx.assert_all(indoc! {"
|
||||||
cx.assert("The qˇuick", "The quickˇ");
|
ˇ
|
||||||
cx.assert("The qˇuick ", "The quick ˇ");
|
The qˇuick
|
||||||
cx.assert("ˇ", "ˇ");
|
brown ˇfox "})
|
||||||
cx.assert(
|
.await;
|
||||||
indoc! {"
|
|
||||||
The qˇuick
|
|
||||||
brown fox"},
|
|
||||||
indoc! {"
|
|
||||||
The quickˇ
|
|
||||||
brown fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
The quick"},
|
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
The quick"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -984,84 +768,45 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
|
async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let cx = NeovimBackedTestContext::new(cx).await;
|
||||||
let mut cx = cx.binding(["shift-o"]).mode_after(Mode::Insert);
|
let mut cx = cx.binding(["shift-o"]);
|
||||||
|
cx.assert("ˇ").await;
|
||||||
|
cx.assert("The ˇquick").await;
|
||||||
|
cx.assert_all(indoc! {"
|
||||||
|
The qˇuick
|
||||||
|
brown ˇfox
|
||||||
|
jumps ˇover"})
|
||||||
|
.await;
|
||||||
|
cx.assert(indoc! {"
|
||||||
|
The quick
|
||||||
|
ˇ
|
||||||
|
brown fox"})
|
||||||
|
.await;
|
||||||
|
|
||||||
cx.assert(
|
// Our indentation is smarter than vims. So we don't match here
|
||||||
"ˇ",
|
cx.assert_manual(
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
"The ˇquick",
|
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
The quick"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
brown ˇfox
|
|
||||||
jumps over"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇ
|
|
||||||
brown fox
|
|
||||||
jumps over"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
brown fox
|
|
||||||
jumps ˇover"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
brown fox
|
|
||||||
ˇ
|
|
||||||
jumps over"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The qˇuick
|
|
||||||
brown fox
|
|
||||||
jumps over"},
|
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
The quick
|
|
||||||
brown fox
|
|
||||||
jumps over"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇ
|
|
||||||
brown fox"},
|
|
||||||
indoc! {"
|
|
||||||
The quick
|
|
||||||
ˇ
|
|
||||||
|
|
||||||
brown fox"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
indoc! {"
|
||||||
fn test()
|
fn test()
|
||||||
println!(ˇ);"},
|
println!(ˇ);"},
|
||||||
|
Mode::Normal,
|
||||||
indoc! {"
|
indoc! {"
|
||||||
fn test()
|
fn test()
|
||||||
ˇ
|
ˇ
|
||||||
println!();"},
|
println!();"},
|
||||||
|
Mode::Insert,
|
||||||
);
|
);
|
||||||
cx.assert(
|
cx.assert_manual(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
fn test(ˇ) {
|
fn test(ˇ) {
|
||||||
println!();
|
println!();
|
||||||
}"},
|
}"},
|
||||||
|
Mode::Normal,
|
||||||
indoc! {"
|
indoc! {"
|
||||||
ˇ
|
ˇ
|
||||||
fn test() {
|
fn test() {
|
||||||
println!();
|
println!();
|
||||||
}"},
|
}"},
|
||||||
|
Mode::Insert,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1208,4 +953,22 @@ mod test {
|
||||||
Mode::Normal,
|
Mode::Normal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for count in 1..=5 {
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
[&count.to_string(), "w"],
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quˇickˇ browˇn
|
||||||
|
ˇ
|
||||||
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
|
ˇthe lazy dog
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,20 @@
|
||||||
use crate::{motion::Motion, object::Object, 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 editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
|
||||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
use gpui::MutableAppContext;
|
||||||
use serde::Deserialize;
|
use language::Selection;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
#[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_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
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);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
@ -56,38 +46,30 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||||
// white space after a word, they only change up to the end of the word. This is
|
// 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
|
// because Vim interprets "cw" as change-word, and a word does not include the
|
||||||
// following white space.
|
// following white space.
|
||||||
fn change_word(
|
fn expand_changed_word_selection(
|
||||||
_: &mut Workspace,
|
map: &DisplaySnapshot,
|
||||||
&ChangeWord { ignore_punctuation }: &ChangeWord,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
times: usize,
|
||||||
|
ignore_punctuation: bool,
|
||||||
) {
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
if times > 1 {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||||
editor.transact(cx, |editor, cx| {
|
map,
|
||||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
selection,
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
times - 1,
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
false,
|
||||||
s.move_with(|map, selection| {
|
);
|
||||||
if selection.end.column() == map.line_len(selection.end.row()) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.end =
|
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
|
||||||
movement::find_boundary(map, selection.end, |left, right| {
|
return;
|
||||||
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'
|
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);
|
||||||
});
|
|
||||||
copy_selections_content(editor, false, cx);
|
left_kind != right_kind || left == '\n' || right == '\n'
|
||||||
editor.insert("", cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +77,10 @@ fn change_word(
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{state::Mode, test_contexts::VimTestContext};
|
use crate::{
|
||||||
|
state::Mode,
|
||||||
|
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -459,4 +444,85 @@ mod test {
|
||||||
the lazy"},
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use collections::{HashMap, HashSet};
|
||||||
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
|
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
pub fn delete_motion(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| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
@ -11,8 +11,8 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext)
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let original_head = selection.head();
|
let original_head = selection.head();
|
||||||
motion.expand_selection(map, selection, true);
|
|
||||||
original_columns.insert(selection.id, original_head.column());
|
original_columns.insert(selection.id, original_head.column());
|
||||||
|
motion.expand_selection(map, selection, times, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
copy_selections_content(editor, motion.linewise(), cx);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
pub fn yank_motion(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| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
@ -10,8 +10,8 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let original_position = (selection.head(), selection.goal);
|
let original_position = (selection.head(), selection.goal);
|
||||||
motion.expand_selection(map, selection, true);
|
|
||||||
original_positions.insert(selection.id, original_position);
|
original_positions.insert(selection.id, original_position);
|
||||||
|
motion.expand_selection(map, selection, times, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
copy_selections_content(editor, motion.linewise(), cx);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
|
|
@ -12,7 +12,6 @@ use crate::{motion, normal::normal_object, state::Mode, visual::visual_object, V
|
||||||
pub enum Object {
|
pub enum Object {
|
||||||
Word { ignore_punctuation: bool },
|
Word { ignore_punctuation: bool },
|
||||||
Sentence,
|
Sentence,
|
||||||
Paragraph,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -22,7 +21,7 @@ struct Word {
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(vim, [Sentence, Paragraph]);
|
actions!(vim, [Sentence]);
|
||||||
impl_actions!(vim, [Word]);
|
impl_actions!(vim, [Word]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
@ -32,7 +31,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, 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) {
|
fn object(object: Object, cx: &mut MutableAppContext) {
|
||||||
|
@ -61,7 +59,6 @@ impl Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object::Sentence => sentence(map, relative_to, around),
|
Object::Sentence => sentence(map, relative_to, around),
|
||||||
_ => relative_to..relative_to,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,71 +169,19 @@ fn around_next_word(
|
||||||
start..end
|
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> {
|
fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> Range<DisplayPoint> {
|
||||||
let mut start = None;
|
let mut start = None;
|
||||||
let mut previous_end = relative_to;
|
let mut previous_end = relative_to;
|
||||||
|
|
||||||
for (char, point) in map.reverse_chars_at(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) {
|
if is_sentence_end(map, point) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -248,36 +193,26 @@ fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> R
|
||||||
previous_end = point;
|
previous_end = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle case where cursor was before the sentence start
|
// Search forward for the end of the current sentence or if we are between sentences, the start of the next one
|
||||||
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;
|
let mut end = relative_to;
|
||||||
for (char, point) in chars {
|
for (char, point) in chars {
|
||||||
if start.is_some() {
|
if start.is_none() && is_possible_sentence_start(char) {
|
||||||
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 {
|
if around {
|
||||||
start = Some(point);
|
start = Some(point);
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
end = point;
|
end = point;
|
||||||
break;
|
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;
|
let mut range = start.unwrap_or(previous_end)..end;
|
||||||
|
@ -296,22 +231,21 @@ const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
|
||||||
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
|
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
|
||||||
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
|
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
|
||||||
fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
let mut chars = map.chars_at(point).peekable();
|
let mut next_chars = map.chars_at(point).peekable();
|
||||||
|
if let Some((char, _)) = next_chars.next() {
|
||||||
if let Some((char, _)) = chars.next() {
|
// We are at a double newline. This position is a sentence end.
|
||||||
if char == '\n' && chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !SENTENCE_END_PUNCTUATION.contains(&char) {
|
// The next text is not a valid whitespace. This is not a sentence end
|
||||||
|
if !SENTENCE_END_WHITESPACE.contains(&char) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (char, _) in chars {
|
for (char, _) in map.reverse_chars_at(point) {
|
||||||
if SENTENCE_END_WHITESPACE.contains(&char) {
|
if SENTENCE_END_PUNCTUATION.contains(&char) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +254,7 @@ fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
||||||
|
@ -331,16 +265,26 @@ fn expand_to_include_whitespace(
|
||||||
stop_at_newline: bool,
|
stop_at_newline: bool,
|
||||||
) -> Range<DisplayPoint> {
|
) -> Range<DisplayPoint> {
|
||||||
let mut whitespace_included = false;
|
let mut whitespace_included = false;
|
||||||
for (char, point) in map.chars_at(range.end) {
|
|
||||||
range.end = point;
|
|
||||||
|
|
||||||
|
let mut chars = map.chars_at(range.end).peekable();
|
||||||
|
while let Some((char, point)) = chars.next() {
|
||||||
if char == '\n' && stop_at_newline {
|
if char == '\n' && stop_at_newline {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if char.is_whitespace() {
|
if char.is_whitespace() {
|
||||||
whitespace_included = true;
|
// 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 {
|
} else {
|
||||||
|
// Found non whitespace. Quit out.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,7 +329,7 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_in_word(cx: &mut gpui::TestAppContext) {
|
async fn test_change_in_word(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_change_in_word", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["c", "i", "w"]);
|
.binding(["c", "i", "w"]);
|
||||||
cx.assert_all(WORD_LOCATIONS).await;
|
cx.assert_all(WORD_LOCATIONS).await;
|
||||||
|
@ -395,7 +339,7 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_in_word(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_in_word(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_delete_in_word", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["d", "i", "w"]);
|
.binding(["d", "i", "w"]);
|
||||||
cx.assert_all(WORD_LOCATIONS).await;
|
cx.assert_all(WORD_LOCATIONS).await;
|
||||||
|
@ -405,7 +349,7 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_around_word(cx: &mut gpui::TestAppContext) {
|
async fn test_change_around_word(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_change_around_word", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["c", "a", "w"]);
|
.binding(["c", "a", "w"]);
|
||||||
cx.assert_all(WORD_LOCATIONS).await;
|
cx.assert_all(WORD_LOCATIONS).await;
|
||||||
|
@ -415,7 +359,7 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_around_word(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_around_word(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_delete_around_word", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["d", "a", "w"]);
|
.binding(["d", "a", "w"]);
|
||||||
cx.assert_all(WORD_LOCATIONS).await;
|
cx.assert_all(WORD_LOCATIONS).await;
|
||||||
|
@ -431,7 +375,8 @@ mod test {
|
||||||
the lazy doˇgˇ.ˇ ˇThe quick ˇ
|
the lazy doˇgˇ.ˇ ˇThe quick ˇ
|
||||||
brown fox jumps over
|
brown fox jumps over
|
||||||
"},
|
"},
|
||||||
// Double newlines are broken currently
|
// 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! {"
|
// indoc! {"
|
||||||
// The quick brown fox jumps.
|
// The quick brown fox jumps.
|
||||||
// Over the lazy dog
|
// Over the lazy dog
|
||||||
|
@ -441,12 +386,12 @@ mod test {
|
||||||
// the lazy dog.ˇ
|
// the lazy dog.ˇ
|
||||||
// ˇ
|
// ˇ
|
||||||
// "},
|
// "},
|
||||||
r#"The quick brown.)]'" Brown fox jumps."#,
|
r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
|
||||||
];
|
];
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_in_sentence(cx: &mut gpui::TestAppContext) {
|
async fn test_change_in_sentence(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_change_in_sentence", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["c", "i", "s"]);
|
.binding(["c", "i", "s"]);
|
||||||
for sentence_example in SENTENCE_EXAMPLES {
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
@ -456,31 +401,42 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_in_sentence(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_in_sentence(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_delete_in_sentence", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["d", "i", "s"]);
|
.binding(["d", "i", "s"]);
|
||||||
|
|
||||||
for sentence_example in SENTENCE_EXAMPLES {
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
cx.assert_all(sentence_example).await;
|
cx.assert_all(sentence_example).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[ignore] // End cursor position is incorrect
|
|
||||||
async fn test_change_around_sentence(cx: &mut gpui::TestAppContext) {
|
async fn test_change_around_sentence(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_change_around_sentence", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["c", "a", "s"]);
|
.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 {
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
cx.assert_all(sentence_example).await;
|
cx.assert_all(sentence_example).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[ignore] // End cursor position is incorrect
|
|
||||||
async fn test_delete_around_sentence(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_around_sentence(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_delete_around_sentence", cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["d", "a", "s"]);
|
.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 {
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
cx.assert_all(sentence_example).await;
|
cx.assert_all(sentence_example).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub enum Namespace {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub enum Operator {
|
pub enum Operator {
|
||||||
|
Number(usize),
|
||||||
Namespace(Namespace),
|
Namespace(Namespace),
|
||||||
Change,
|
Change,
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -92,12 +93,14 @@ impl VimState {
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
||||||
let operator_context = match operator {
|
let operator_context = match operator {
|
||||||
|
Some(Operator::Number(_)) => "n",
|
||||||
Some(Operator::Namespace(Namespace::G)) => "g",
|
Some(Operator::Namespace(Namespace::G)) => "g",
|
||||||
Some(Operator::Object { around: false }) => "i",
|
Some(Operator::Object { around: false }) => "i",
|
||||||
Some(Operator::Object { around: true }) => "a",
|
Some(Operator::Object { around: true }) => "a",
|
||||||
Some(Operator::Change) => "c",
|
Some(Operator::Change) => "c",
|
||||||
Some(Operator::Delete) => "d",
|
Some(Operator::Delete) => "d",
|
||||||
Some(Operator::Yank) => "y",
|
Some(Operator::Yank) => "y",
|
||||||
|
|
||||||
None => "none",
|
None => "none",
|
||||||
}
|
}
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use util::test::marked_text_offsets;
|
use crate::state::Mode;
|
||||||
|
|
||||||
use super::NeovimBackedTestContext;
|
use super::NeovimBackedTestContext;
|
||||||
|
|
||||||
|
@ -24,20 +24,39 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||||
self.cx
|
self.cx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert(&mut self, initial_state: &str) {
|
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) {
|
||||||
self.cx
|
self.cx
|
||||||
.assert_binding_matches(self.keystrokes_under_test, initial_state)
|
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_all(&mut self, marked_positions: &str) {
|
pub fn assert_manual(
|
||||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
&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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for cursor_offset in cursor_offsets.iter() {
|
pub async fn assert_all(&mut self, marked_positions: &str) {
|
||||||
let mut marked_text = unmarked_text.clone();
|
self.cx
|
||||||
marked_text.insert(*cursor_offset, 'ˇ');
|
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
|
||||||
self.assert(&marked_text).await;
|
.await
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use editor::DisplayPoint;
|
use editor::DisplayPoint;
|
||||||
use gpui::keymap::Keystroke;
|
use gpui::keymap::Keystroke;
|
||||||
|
|
||||||
|
@ -14,11 +15,13 @@ use async_trait::async_trait;
|
||||||
use nvim_rs::{
|
use nvim_rs::{
|
||||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use tokio::{
|
use tokio::{
|
||||||
process::{Child, ChildStdin, Command},
|
process::{Child, ChildStdin, Command},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
|
use util::test::marked_text_offsets;
|
||||||
|
|
||||||
use crate::state::Mode;
|
use crate::state::Mode;
|
||||||
|
|
||||||
|
@ -26,60 +29,43 @@ use super::{NeovimBackedBindingTestContext, VimTestContext};
|
||||||
|
|
||||||
pub struct NeovimBackedTestContext<'a> {
|
pub struct NeovimBackedTestContext<'a> {
|
||||||
cx: VimTestContext<'a>,
|
cx: VimTestContext<'a>,
|
||||||
test_case_id: &'static str,
|
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||||
data_counter: usize,
|
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||||
#[cfg(feature = "neovim")]
|
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||||
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
neovim: NeovimConnection,
|
||||||
#[cfg(feature = "neovim")]
|
|
||||||
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
|
||||||
#[cfg(feature = "neovim")]
|
|
||||||
_child: Child,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NeovimBackedTestContext<'a> {
|
impl<'a> NeovimBackedTestContext<'a> {
|
||||||
pub async fn new(
|
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||||
test_case_id: &'static str,
|
let function_name = cx.function_name.clone();
|
||||||
cx: &'a mut gpui::TestAppContext,
|
|
||||||
) -> NeovimBackedTestContext<'a> {
|
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
|
Self {
|
||||||
#[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,
|
cx,
|
||||||
test_case_id,
|
exemptions: Default::default(),
|
||||||
data_counter: 0,
|
neovim: NeovimConnection::new(function_name).await,
|
||||||
#[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 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 fn add_keybinding_exemption<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keybinding: [&str; COUNT],
|
||||||
|
initial_state: &str,
|
||||||
|
) {
|
||||||
|
let initial_state = initial_state.to_string();
|
||||||
|
let exempted_keybindings = self
|
||||||
|
.exemptions
|
||||||
|
.entry(initial_state)
|
||||||
|
.or_insert(Some(Default::default()));
|
||||||
|
|
||||||
|
if let Some(exempted_bindings) = exempted_keybindings.as_mut() {
|
||||||
|
exempted_bindings.insert(format!("{keybinding:?}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) {
|
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) {
|
||||||
|
@ -101,7 +87,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
|
|
||||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||||
|
|
||||||
self.nvim
|
self.neovim
|
||||||
.input(&key)
|
.input(&key)
|
||||||
.await
|
.await
|
||||||
.expect("Could not input keystroke");
|
.expect("Could not input keystroke");
|
||||||
|
@ -128,37 +114,32 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
let cursor_point =
|
let cursor_point =
|
||||||
self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||||
let nvim_buffer = self
|
let nvim_buffer = self
|
||||||
.nvim
|
.neovim
|
||||||
.get_current_buf()
|
.get_current_buf()
|
||||||
.await
|
.await
|
||||||
.expect("Could not get neovim buffer");
|
.expect("Could not get neovim buffer");
|
||||||
let mut lines = self
|
let mut lines = self
|
||||||
.buffer_text()
|
.buffer_text()
|
||||||
.lines()
|
.split('\n')
|
||||||
.map(|line| line.to_string())
|
.map(|line| line.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if lines.len() > 1 {
|
|
||||||
// Add final newline which is missing from buffer_text
|
|
||||||
lines.push("".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
nvim_buffer
|
nvim_buffer
|
||||||
.set_lines(0, -1, false, lines)
|
.set_lines(0, -1, false, lines)
|
||||||
.await
|
.await
|
||||||
.expect("Could not set nvim buffer text");
|
.expect("Could not set nvim buffer text");
|
||||||
|
|
||||||
self.nvim
|
self.neovim
|
||||||
.input("<escape>")
|
.input("<escape>")
|
||||||
.await
|
.await
|
||||||
.expect("Could not send escape to nvim");
|
.expect("Could not send escape to nvim");
|
||||||
self.nvim
|
self.neovim
|
||||||
.input("<escape>")
|
.input("<escape>")
|
||||||
.await
|
.await
|
||||||
.expect("Could not send escape to nvim");
|
.expect("Could not send escape to nvim");
|
||||||
|
|
||||||
let nvim_window = self
|
let nvim_window = self
|
||||||
.nvim
|
.neovim
|
||||||
.get_current_win()
|
.get_current_win()
|
||||||
.await
|
.await
|
||||||
.expect("Could not get neovim window");
|
.expect("Could not get neovim window");
|
||||||
|
@ -173,18 +154,161 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_state_matches(&mut self) {
|
pub async fn assert_state_matches(&mut self) {
|
||||||
assert_eq!(self.neovim_text().await, self.buffer_text());
|
assert_eq!(
|
||||||
|
self.neovim.text().await,
|
||||||
|
self.buffer_text(),
|
||||||
|
"{}",
|
||||||
|
self.assertion_context.context()
|
||||||
|
);
|
||||||
|
|
||||||
let zed_head = self.update_editor(|editor, cx| editor.selections.newest_display(cx).head());
|
let zed_head = self.update_editor(|editor, cx| editor.selections.newest_display(cx).head());
|
||||||
assert_eq!(self.neovim_head().await, zed_head);
|
assert_eq!(
|
||||||
|
self.neovim.head().await,
|
||||||
|
zed_head,
|
||||||
|
"{}",
|
||||||
|
self.assertion_context.context()
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(neovim_mode) = self.neovim_mode().await {
|
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||||
assert_eq!(neovim_mode, self.mode());
|
assert_eq!(
|
||||||
|
neovim_mode,
|
||||||
|
self.mode(),
|
||||||
|
"{}",
|
||||||
|
self.assertion_context.context()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystrokes: [&str; COUNT],
|
||||||
|
initial_state: &str,
|
||||||
|
) {
|
||||||
|
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 => {
|
||||||
|
// All keystrokes for this insertion text are exempted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _keybinding_context_handle =
|
||||||
|
self.add_assertion_context(format!("Key Binding Under Test: {:?}", keystrokes));
|
||||||
|
let _initial_state_context_handle = self.add_assertion_context(format!(
|
||||||
|
"Initial State: \"{}\"",
|
||||||
|
initial_state.escape_debug().to_string()
|
||||||
|
));
|
||||||
|
self.set_shared_state(initial_state).await;
|
||||||
|
self.simulate_shared_keystrokes(keystrokes).await;
|
||||||
|
self.assert_state_matches().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum NeovimData {
|
||||||
|
Text(String),
|
||||||
|
Head { row: u32, column: u32 },
|
||||||
|
Mode(Option<Mode>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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.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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
pub async fn neovim_text(&mut self) -> String {
|
pub async fn text(&mut self) -> String {
|
||||||
let nvim_buffer = self
|
let nvim_buffer = self
|
||||||
.nvim
|
.nvim
|
||||||
.get_current_buf()
|
.get_current_buf()
|
||||||
|
@ -196,17 +320,22 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
.expect("Could not get buffer text")
|
.expect("Could not get buffer text")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
self.write_test_data(text.clone(), "text");
|
self.data.push_back(NeovimData::Text(text.clone()));
|
||||||
|
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "neovim"))]
|
#[cfg(not(feature = "neovim"))]
|
||||||
pub async fn neovim_text(&mut self) -> String {
|
pub async fn text(&mut self) -> String {
|
||||||
self.read_test_data("text")
|
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")]
|
#[cfg(feature = "neovim")]
|
||||||
pub async fn neovim_head(&mut self) -> DisplayPoint {
|
pub async fn head(&mut self) -> DisplayPoint {
|
||||||
let nvim_row: u32 = self
|
let nvim_row: u32 = self
|
||||||
.nvim
|
.nvim
|
||||||
.command_output("echo line('.')")
|
.command_output("echo line('.')")
|
||||||
|
@ -224,24 +353,25 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
- 1; // Neovim columns start at 1
|
- 1; // Neovim columns start at 1
|
||||||
|
|
||||||
let serialized = format!("{},{}", nvim_row.to_string(), nvim_column.to_string());
|
self.data.push_back(NeovimData::Head {
|
||||||
self.write_test_data(serialized, "head");
|
row: nvim_row,
|
||||||
|
column: nvim_column,
|
||||||
|
});
|
||||||
|
|
||||||
DisplayPoint::new(nvim_row, nvim_column)
|
DisplayPoint::new(nvim_row, nvim_column)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "neovim"))]
|
#[cfg(not(feature = "neovim"))]
|
||||||
pub async fn neovim_head(&mut self) -> DisplayPoint {
|
pub async fn head(&mut self) -> DisplayPoint {
|
||||||
let serialized = self.read_test_data("head");
|
if let Some(NeovimData::Head { row, column }) = self.data.pop_front() {
|
||||||
let mut components = serialized.split(',');
|
DisplayPoint::new(row, column)
|
||||||
let nvim_row = components.next().unwrap().parse::<u32>().unwrap();
|
} else {
|
||||||
let nvim_column = components.next().unwrap().parse::<u32>().unwrap();
|
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||||
|
}
|
||||||
DisplayPoint::new(nvim_row, nvim_column)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
pub async fn neovim_mode(&mut self) -> Option<Mode> {
|
pub async fn mode(&mut self) -> Option<Mode> {
|
||||||
let nvim_mode_text = self
|
let nvim_mode_text = self
|
||||||
.nvim
|
.nvim
|
||||||
.get_mode()
|
.get_mode()
|
||||||
|
@ -265,74 +395,67 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&mode).expect("Could not serialize mode");
|
self.data.push_back(NeovimData::Mode(mode.clone()));
|
||||||
|
|
||||||
self.write_test_data(serialized, "mode");
|
|
||||||
|
|
||||||
mode
|
mode
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "neovim"))]
|
#[cfg(not(feature = "neovim"))]
|
||||||
pub async fn neovim_mode(&mut self) -> Option<Mode> {
|
pub async fn mode(&mut self) -> Option<Mode> {
|
||||||
let serialized = self.read_test_data("mode");
|
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
|
||||||
serde_json::from_str(&serialized).expect("Could not deserialize test data")
|
mode
|
||||||
|
} else {
|
||||||
|
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_data_directory(&self) -> PathBuf {
|
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
data_path.push("test_data");
|
data_path.push("test_data");
|
||||||
data_path.push(self.test_case_id);
|
data_path.push(format!("{}.json", 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
|
data_path
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "neovim"))]
|
#[cfg(not(feature = "neovim"))]
|
||||||
fn read_test_data(&mut self, kind: &str) -> String {
|
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||||
let path = self.next_data_path(kind);
|
let path = Self::test_data_path(test_case_id);
|
||||||
std::fs::read_to_string(path).expect(
|
let json = std::fs::read_to_string(path).expect(
|
||||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "neovim")]
|
serde_json::from_str(&json)
|
||||||
fn write_test_data(&mut self, data: String, kind: &str) {
|
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Clone)]
|
||||||
struct NvimHandler {}
|
struct NvimHandler {}
|
||||||
|
|
||||||
|
@ -359,16 +482,17 @@ impl Handler for NvimHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for NeovimBackedTestContext<'a> {
|
#[cfg(test)]
|
||||||
type Target = VimTestContext<'a>;
|
mod test {
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
use crate::test_contexts::NeovimBackedTestContext;
|
||||||
&self.cx
|
|
||||||
}
|
#[gpui::test]
|
||||||
}
|
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
cx.assert_state_matches().await;
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
cx.set_shared_state("This is a tesˇt").await;
|
||||||
&mut self.cx
|
cx.assert_state_matches().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use editor::test::EditorTestContext;
|
use editor::test::{AssertionContextManager, EditorTestContext};
|
||||||
use gpui::{json::json, AppContext, ViewHandle};
|
use gpui::{json::json, AppContext, ViewHandle};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use search::{BufferSearchBar, ProjectSearchBar};
|
use search::{BufferSearchBar, ProjectSearchBar};
|
||||||
|
@ -82,6 +82,7 @@ impl<'a> VimTestContext<'a> {
|
||||||
cx,
|
cx,
|
||||||
window_id,
|
window_id,
|
||||||
editor,
|
editor,
|
||||||
|
assertion_context: AssertionContextManager::new(),
|
||||||
},
|
},
|
||||||
workspace,
|
workspace,
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,10 @@ pub struct SwitchMode(pub Mode);
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
pub struct PushOperator(pub Operator);
|
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) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
editor_events::init(cx);
|
editor_events::init(cx);
|
||||||
|
@ -45,6 +48,9 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
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
|
// Editor Actions
|
||||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||||
|
@ -145,6 +151,15 @@ impl Vim {
|
||||||
self.sync_vim_settings(cx);
|
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 {
|
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
||||||
let popped_operator = self.state.operator_stack.pop()
|
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");
|
.expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||||
|
@ -152,6 +167,15 @@ impl Vim {
|
||||||
popped_operator
|
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) {
|
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
||||||
self.state.operator_stack.clear();
|
self.state.operator_stack.clear();
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
|
@ -227,7 +251,7 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new("test_neovim", cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
cx.simulate_shared_keystroke("i").await;
|
cx.simulate_shared_keystroke("i").await;
|
||||||
cx.simulate_shared_keystrokes([
|
cx.simulate_shared_keystrokes([
|
||||||
|
|
|
@ -17,14 +17,18 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(paste);
|
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(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
|
|
||||||
let was_reversed = selection.reversed;
|
let was_reversed = selection.reversed;
|
||||||
selection.set_head(new_head, goal);
|
|
||||||
|
for _ in 0..times {
|
||||||
|
let (new_head, goal) =
|
||||||
|
motion.move_point(map, selection.head(), selection.goal);
|
||||||
|
selection.set_head(new_head, goal);
|
||||||
|
}
|
||||||
|
|
||||||
if was_reversed && !selection.reversed {
|
if was_reversed && !selection.reversed {
|
||||||
// Head was at the start of the selection, and now is at the end. We need to move the start
|
// Head was at the start of the selection, and now is at the end. We need to move the start
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":""},{"Head":{"row":0,"column":0}},{"Mode":"Normal"},{"Text":"This is a test"},{"Head":{"row":0,"column":13}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_a.json
Normal file
1
crates/vim/test_data/test_a.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick"},{"Head":{"row":0,"column":6}},{"Mode":"Insert"},{"Text":"The quick"},{"Head":{"row":0,"column":9}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_backspace.json
Normal file
1
crates/vim/test_data/test_backspace.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown"},{"Head":{"row":0,"column":0}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Head":{"row":0,"column":4}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Head":{"row":0,"column":8}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_change_around_sentence.json
Normal file
1
crates/vim/test_data/test_change_around_sentence.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"Fox Jumps! Over the lazy."},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Fox Jumps! Over the lazy."},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Fox Jumps! Over the lazy."},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Head":{"row":0,"column":17}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Head":{"row":0,"column":17}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Head":{"row":0,"column":17}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Head":{"row":0,"column":27}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Head":{"row":0,"column":27}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Head":{"row":0,"column":27}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Head":{"row":0,"column":27}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog.\n"},{"Head":{"row":2,"column":13}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog.\n"},{"Head":{"row":2,"column":13}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog.\n"},{"Head":{"row":2,"column":13}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"Brown fox jumps. "},{"Head":{"row":0,"column":0}},{"Mode":"Insert"},{"Text":"The quick brown.)]'\" "},{"Head":{"row":0,"column":21}},{"Mode":"Insert"},{"Text":"The quick brown.)]'\" "},{"Head":{"row":0,"column":21}},{"Mode":"Insert"}]
|
|
@ -1 +0,0 @@
|
||||||
Fox Jumps! Over the lazy.
|
|
|
@ -1 +0,0 @@
|
||||||
0,16
|
|
|
@ -1 +0,0 @@
|
||||||
0,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
Fox Jumps! Over the lazy.
|
|
|
@ -1 +0,0 @@
|
||||||
0,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
Fox Jumps! Over the lazy.
|
|
|
@ -1 +0,0 @@
|
||||||
0,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
The quick brown? Over the lazy.
|
|
1
crates/vim/test_data/test_change_around_word.json
Normal file
1
crates/vim/test_data/test_change_around_word.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,12 +0,0 @@
|
||||||
The quick
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,0
|
|
|
@ -1 +0,0 @@
|
||||||
1,4
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,9
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,10
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,9 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,15
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,10 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
7,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
8,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
9,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
9,2
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
|
@ -1 +0,0 @@
|
||||||
10,12
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
|
@ -1 +0,0 @@
|
||||||
11,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
1,4
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
1,9
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
2,12
|
|
|
@ -1 +0,0 @@
|
||||||
0,10
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
3,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
The-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
4,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,11 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
5,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
|
@ -1,12 +0,0 @@
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-quick brown
|
|
||||||
|
|
||||||
|
|
||||||
fox-jumps over
|
|
||||||
the lazy dog
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
6,0
|
|
|
@ -1 +0,0 @@
|
||||||
"Insert"
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue