Add assertion context manager to TestAppContext and convert existing vim tests to use neovim backed test context
This commit is contained in:
parent
5fec8c8bfd
commit
d2494822b0
41 changed files with 2062 additions and 2212 deletions
|
@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test_contexts::VimTestContext};
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
|
|
|
@ -10,8 +10,10 @@ use crate::{
|
|||
state::{Mode, Operator},
|
||||
Vim,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint,
|
||||
};
|
||||
use gpui::{actions, MutableAppContext, ViewContext};
|
||||
use language::{AutoindentMode, Point, SelectionGoal};
|
||||
use workspace::Workspace;
|
||||
|
@ -258,7 +260,18 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
clipboard_text = Cow::Owned(newline_separated_text);
|
||||
}
|
||||
|
||||
let mut new_selections = Vec::new();
|
||||
// If the pasted text is a single line, the cursor should be placed after
|
||||
// the newly pasted text. This is easiest done with an anchor after the
|
||||
// insertion, and then with a fixup to move the selection back one position.
|
||||
// However if the pasted text is linewise, the cursor should be placed at the start
|
||||
// of the new text on the following line. This is easiest done with a manually adjusted
|
||||
// point.
|
||||
// This enum lets us represent both cases
|
||||
enum NewPosition {
|
||||
Inside(Point),
|
||||
After(Anchor),
|
||||
}
|
||||
let mut new_selections: HashMap<usize, NewPosition> = Default::default();
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let mut start_offset = 0;
|
||||
|
@ -288,8 +301,10 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
edits.push((point..point, "\n"));
|
||||
}
|
||||
// Drop selection at the start of the next line
|
||||
let selection_point = Point::new(point.row + 1, 0);
|
||||
new_selections.push(selection.map(|_| selection_point));
|
||||
new_selections.insert(
|
||||
selection.id,
|
||||
NewPosition::Inside(Point::new(point.row + 1, 0)),
|
||||
);
|
||||
point
|
||||
} else {
|
||||
let mut point = selection.end;
|
||||
|
@ -299,7 +314,14 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
.clip_point(point, Bias::Right)
|
||||
.to_point(&display_map);
|
||||
|
||||
new_selections.push(selection.map(|_| point));
|
||||
new_selections.insert(
|
||||
selection.id,
|
||||
if to_insert.contains('\n') {
|
||||
NewPosition::Inside(point)
|
||||
} else {
|
||||
NewPosition::After(snapshot.anchor_after(point))
|
||||
},
|
||||
);
|
||||
point
|
||||
};
|
||||
|
||||
|
@ -317,7 +339,25 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
});
|
||||
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections)
|
||||
s.move_with(|map, selection| {
|
||||
if let Some(new_position) = new_selections.get(&selection.id) {
|
||||
match new_position {
|
||||
NewPosition::Inside(new_point) => {
|
||||
selection.collapse_to(
|
||||
new_point.to_display_point(map),
|
||||
SelectionGoal::None,
|
||||
);
|
||||
}
|
||||
NewPosition::After(after_point) => {
|
||||
let mut new_point = after_point.to_display_point(map);
|
||||
*new_point.column_mut() =
|
||||
new_point.column().saturating_sub(1);
|
||||
new_point = map.clip_point(new_point, Bias::Left);
|
||||
selection.collapse_to(new_point, SelectionGoal::None);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
editor.insert(&clipboard_text, cx);
|
||||
|
@ -332,14 +372,13 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use crate::{
|
||||
state::{
|
||||
Mode::{self, *},
|
||||
Namespace, Operator,
|
||||
},
|
||||
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -476,48 +515,22 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
||||
ˇˇThe ˇquickˇ-ˇbrown
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthe"});
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick-brown
|
||||
|
||||
|
||||
fox_jumps over
|
||||
thˇe"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
for cursor_offset in cursor_offsets.into_iter().rev() {
|
||||
cx.simulate_keystroke("b");
|
||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
||||
}
|
||||
|
||||
// Reset and test ignoring punctuation
|
||||
let (_, cursor_offsets) = marked_text_offsets(indoc! {"
|
||||
ˇˇThe ˇquick-brown
|
||||
ˇthe"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-b"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthe"});
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick-brown
|
||||
|
||||
|
||||
fox_jumps over
|
||||
thˇe"},
|
||||
Mode::Normal,
|
||||
);
|
||||
for cursor_offset in cursor_offsets.into_iter().rev() {
|
||||
cx.simulate_keystroke("shift-b");
|
||||
cx.assert_editor_selections(vec![cursor_offset..cursor_offset]);
|
||||
}
|
||||
ˇthe"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -571,199 +584,98 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["^"]);
|
||||
cx.assert("The qˇuick", "ˇThe quick");
|
||||
cx.assert(" The qˇuick", " ˇThe quick");
|
||||
cx.assert("ˇ", "ˇ");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
|
||||
cx.assert("The qˇuick").await;
|
||||
cx.assert(" The qˇuick").await;
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
ˇThe quick
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
ˇ
|
||||
The quick"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
The quick"},
|
||||
);
|
||||
The quick"})
|
||||
.await;
|
||||
// Indoc disallows trailing whitspace.
|
||||
cx.assert(" ˇ \nThe quick", " ˇ \nThe quick");
|
||||
cx.assert(" ˇ \nThe quick").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-i"]).mode_after(Mode::Insert);
|
||||
cx.assert("The qˇuick", "ˇThe quick");
|
||||
cx.assert(" The qˇuick", " ˇThe quick");
|
||||
cx.assert("ˇ", "ˇ");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
|
||||
cx.assert("The qˇuick").await;
|
||||
cx.assert(" The qˇuick").await;
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
ˇThe quick
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
ˇ
|
||||
The quick"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
The quick"},
|
||||
);
|
||||
The quick"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-d"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The ˇq
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
);
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_x(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["x"]);
|
||||
cx.assert("ˇTest", "ˇest");
|
||||
cx.assert("Teˇst", "Teˇt");
|
||||
cx.assert("Tesˇt", "Teˇs");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
|
||||
cx.assert_all("ˇTeˇsˇt").await;
|
||||
cx.assert(indoc! {"
|
||||
Tesˇt
|
||||
test"},
|
||||
indoc! {"
|
||||
Teˇs
|
||||
test"},
|
||||
);
|
||||
test"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_left(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-x"]);
|
||||
cx.assert("Teˇst", "Tˇst");
|
||||
cx.assert("Tˇest", "ˇest");
|
||||
cx.assert("ˇTest", "ˇTest");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
|
||||
cx.assert_all("ˇTˇeˇsˇt").await;
|
||||
cx.assert(indoc! {"
|
||||
Test
|
||||
ˇtest"},
|
||||
indoc! {"
|
||||
Test
|
||||
ˇtest"},
|
||||
);
|
||||
ˇtest"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_o(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["o"]).mode_after(Mode::Insert);
|
||||
|
||||
cx.assert(
|
||||
"ˇ",
|
||||
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! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|
||||
ˇ
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
fn test() {
|
||||
println!(ˇ);
|
||||
}
|
||||
"},
|
||||
indoc! {"
|
||||
fn test() {
|
||||
println!();
|
||||
ˇ
|
||||
}
|
||||
"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
fn test(ˇ) {
|
||||
println!();
|
||||
}"},
|
||||
indoc! {"
|
||||
fn test() {
|
||||
ˇ
|
||||
println!();
|
||||
}"},
|
||||
);
|
||||
}"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -812,146 +724,66 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_dd(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "d"]);
|
||||
|
||||
cx.assert("ˇ", "ˇ");
|
||||
cx.assert("The ˇquick", "ˇ");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
The quick
|
||||
jumps ˇover"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
brownˇ fox
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇbrown fox"},
|
||||
);
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cc(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "c"]).mode_after(Mode::Insert);
|
||||
|
||||
cx.assert("ˇ", "ˇ");
|
||||
cx.assert("The ˇquick", "ˇ");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The quˇick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
);
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_p(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes(["d", "d"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quick brown
|
||||
the laˇzy dog"});
|
||||
cx.simulate_shared_keystrokes(["d", "d"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
the lazy dog
|
||||
ˇfox jumps over"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpˇ»s over
|
||||
the lazy dog"},
|
||||
Mode::Visual { line: false },
|
||||
);
|
||||
cx.simulate_keystroke("y");
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
fox ˇjumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps oveˇr
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps overˇjumps
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -79,7 +79,7 @@ mod test {
|
|||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -96,7 +96,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
|||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, test_contexts::VimTestContext};
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||
|
|
|
@ -310,7 +310,7 @@ fn expand_to_include_whitespace(
|
|||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test_contexts::NeovimBackedTestContext;
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
const WORD_LOCATIONS: &'static str = indoc! {"
|
||||
The quick ˇbrowˇnˇ
|
||||
|
|
102
crates/vim/src/test.rs
Normal file
102
crates/vim/src/test.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
mod neovim_backed_binding_test_context;
|
||||
mod neovim_backed_test_context;
|
||||
mod neovim_connection;
|
||||
mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_binding_test_context::*;
|
||||
pub use vim_test_context::*;
|
||||
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate_shared_keystroke("i").await;
|
||||
cx.simulate_shared_keystrokes([
|
||||
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
|
||||
])
|
||||
.await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.assert_editor_state("ˇtest");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.blur());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.focus_self());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hˇjkl");
|
||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hTestˇjkl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("/");
|
||||
|
||||
// We now use a weird insert mode with selection when jumping to a single line editor
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
})
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use gpui::ContextHandle;
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
use super::NeovimBackedTestContext;
|
||||
|
@ -31,7 +33,10 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
|
|||
self.consume().binding(keystrokes)
|
||||
}
|
||||
|
||||
pub async fn assert(&mut self, marked_positions: &str) {
|
||||
pub async fn assert(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
) -> Option<(ContextHandle, ContextHandle)> {
|
||||
self.cx
|
||||
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::ContextHandle;
|
||||
use language::OffsetRangeExt;
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
|
||||
pub struct NeovimBackedTestContext<'a> {
|
||||
cx: VimTestContext<'a>,
|
||||
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||
neovim: NeovimConnection,
|
||||
}
|
||||
|
||||
impl<'a> NeovimBackedTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||
let function_name = cx.function_name.clone();
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
Self {
|
||||
cx,
|
||||
exemptions: Default::default(),
|
||||
neovim: NeovimConnection::new(function_name).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
|
||||
let initial_state = initial_state.to_string();
|
||||
// None represents all keybindings being exempted for that initial state
|
||||
self.exemptions.insert(initial_state, None);
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
self.simulate_keystroke(keystroke_text)
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) -> ContextHandle {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
}
|
||||
self.simulate_keystrokes(keystroke_texts)
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||
let context_handle = self.set_state(marked_text, Mode::Normal);
|
||||
|
||||
let selection = self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||
let text = self.buffer_text();
|
||||
self.neovim.set_state(selection, &text).await;
|
||||
|
||||
context_handle
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
assert_eq!(
|
||||
self.neovim.text().await,
|
||||
self.buffer_text(),
|
||||
"{}",
|
||||
self.assertion_context()
|
||||
);
|
||||
|
||||
let mut neovim_selection = self.neovim.selection().await;
|
||||
// Zed selections adjust themselves to make the end point visually make sense
|
||||
if neovim_selection.start > neovim_selection.end {
|
||||
neovim_selection.start.column += 1;
|
||||
}
|
||||
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
|
||||
self.assert_editor_selections(vec![neovim_selection]);
|
||||
|
||||
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
) -> Option<(ContextHandle, ContextHandle)> {
|
||||
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
|
||||
match possible_exempted_keystrokes {
|
||||
Some(exempted_keystrokes) => {
|
||||
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
|
||||
// This keystroke was exempted for this insertion text
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All keystrokes for this insertion text are exempted
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _state_context = self.set_shared_state(initial_state).await;
|
||||
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_state_matches().await;
|
||||
Some((_state_context, _keystroke_context))
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches_all<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
marked_positions: &str,
|
||||
) {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
|
||||
self.assert_binding_matches(keystrokes, &marked_text).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
NeovimBackedBindingTestContext::new(keystrokes, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for NeovimBackedTestContext<'a> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.set_shared_state("This is a tesˇt").await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
}
|
383
crates/vim/src/test/neovim_connection.rs
Normal file
383
crates/vim/src/test/neovim_connection.rs
Normal file
|
@ -0,0 +1,383 @@
|
|||
#[cfg(feature = "neovim")]
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::keymap::Keystroke;
|
||||
use language::{Point, Selection};
|
||||
#[cfg(feature = "neovim")]
|
||||
use lazy_static::lazy_static;
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
};
|
||||
#[cfg(feature = "neovim")]
|
||||
use parking_lot::ReentrantMutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::state::Mode;
|
||||
use collections::VecDeque;
|
||||
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
lazy_static! {
|
||||
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum NeovimData {
|
||||
Text(String),
|
||||
Selection { start: (u32, u32), end: (u32, u32) },
|
||||
Mode(Option<Mode>),
|
||||
}
|
||||
|
||||
pub struct NeovimConnection {
|
||||
data: VecDeque<NeovimData>,
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id: String,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: Child,
|
||||
}
|
||||
|
||||
impl NeovimConnection {
|
||||
pub async fn new(test_case_id: String) -> Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
let handler = NvimHandler {};
|
||||
#[cfg(feature = "neovim")]
|
||||
let (nvim, join_handle, child) = Compat::new(async {
|
||||
// Ensure we don't create neovim connections in parallel
|
||||
let _lock = NEOVIM_LOCK.lock();
|
||||
let (nvim, join_handle, child) = new_child_cmd(
|
||||
&mut Command::new("nvim").arg("--embed").arg("--clean"),
|
||||
handler,
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to neovim process");
|
||||
|
||||
nvim.ui_attach(100, 100, &UiAttachOptions::default())
|
||||
.await
|
||||
.expect("Could not attach to ui");
|
||||
|
||||
// Makes system act a little more like zed in terms of indentation
|
||||
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||
.await
|
||||
.expect("Could not set smartindent on startup");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
data: Default::default(),
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
data: Self::read_test_data(&test_case_id),
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a keystroke to the neovim process.
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
let special = keystroke.shift
|
||||
|| keystroke.ctrl
|
||||
|| keystroke.alt
|
||||
|| keystroke.cmd
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.ctrl { "C-" } else { "" };
|
||||
let alt = if keystroke.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.cmd { "D-" } else { "" };
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
||||
self.nvim
|
||||
.input(&key)
|
||||
.await
|
||||
.expect("Could not input keystroke");
|
||||
}
|
||||
|
||||
// If not running with a live neovim connection, this is a no-op
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let lines = text
|
||||
.split('\n')
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nvim_buffer
|
||||
.set_lines(0, -1, false, lines)
|
||||
.await
|
||||
.expect("Could not set nvim buffer text");
|
||||
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
|
||||
let nvim_window = self
|
||||
.nvim
|
||||
.get_current_win()
|
||||
.await
|
||||
.expect("Could not get neovim window");
|
||||
|
||||
if !selection.is_empty() {
|
||||
panic!("Setting neovim state with non empty selection not yet supported");
|
||||
}
|
||||
let cursor = selection.head();
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn text(&mut self) -> String {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let text = nvim_buffer
|
||||
.get_lines(0, -1, false)
|
||||
.await
|
||||
.expect("Could not get buffer text")
|
||||
.join("\n");
|
||||
|
||||
self.data.push_back(NeovimData::Text(text.clone()));
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn text(&mut self) -> String {
|
||||
if let Some(NeovimData::Text(text)) = self.data.pop_front() {
|
||||
text
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn selection(&mut self) -> Range<Point> {
|
||||
let cursor_row: u32 = self
|
||||
.nvim
|
||||
.command_output("echo line('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim rows start at 1
|
||||
let cursor_col: u32 = self
|
||||
.nvim
|
||||
.command_output("echo col('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim columns start at 1
|
||||
|
||||
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not exit visual mode");
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let (start_row, start_col) = nvim_buffer
|
||||
.get_mark("<")
|
||||
.await
|
||||
.expect("Could not get selection start");
|
||||
let (end_row, end_col) = nvim_buffer
|
||||
.get_mark(">")
|
||||
.await
|
||||
.expect("Could not get selection end");
|
||||
self.nvim
|
||||
.input("gv")
|
||||
.await
|
||||
.expect("Could not reselect visual selection");
|
||||
|
||||
if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
|
||||
(
|
||||
(end_row as u32 - 1, end_col as u32),
|
||||
(start_row as u32 - 1, start_col as u32),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(start_row as u32 - 1, start_col as u32),
|
||||
(end_row as u32 - 1, end_col as u32),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
((cursor_row, cursor_col), (cursor_row, cursor_col))
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Selection { start, end });
|
||||
|
||||
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn selection(&mut self) -> Range<Point> {
|
||||
// Selection code fetches the mode. This emulates that.
|
||||
let _mode = self.mode().await;
|
||||
if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
|
||||
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
let nvim_mode_text = self
|
||||
.nvim
|
||||
.get_mode()
|
||||
.await
|
||||
.expect("Could not get mode")
|
||||
.into_iter()
|
||||
.find_map(|(key, value)| {
|
||||
if key.as_str() == Some("mode") {
|
||||
Some(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Could not find mode value");
|
||||
|
||||
let mode = match nvim_mode_text.as_ref() {
|
||||
"i" => Some(Mode::Insert),
|
||||
"n" => Some(Mode::Normal),
|
||||
"v" => Some(Mode::Visual { line: false }),
|
||||
"V" => Some(Mode::Visual { line: true }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Mode(mode.clone()));
|
||||
|
||||
mode
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
|
||||
mode
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(format!("{}.json", test_case_id));
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let json = std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
);
|
||||
|
||||
serde_json::from_str(&json)
|
||||
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Deref for NeovimConnection {
|
||||
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl DerefMut for NeovimConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Drop for NeovimConnection {
|
||||
fn drop(&mut self) {
|
||||
let path = Self::test_data_path(&self.test_case_id);
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("Could not create test data directory");
|
||||
let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
|
||||
std::fs::write(path, json).expect("Could not write out test data");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[derive(Clone)]
|
||||
struct NvimHandler {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[async_trait]
|
||||
impl Handler for NvimHandler {
|
||||
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) -> Result<Value, Value> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn handle_notify(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use editor::test::{AssertionContextManager, EditorTestContext};
|
||||
use gpui::{json::json, AppContext, ViewHandle};
|
||||
use editor::test::editor_test_context::EditorTestContext;
|
||||
use gpui::{json::json, AppContext, ContextHandle, ViewHandle};
|
||||
use project::Project;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
use workspace::{pane, AppState, WorkspaceHandle};
|
||||
|
@ -82,7 +82,6 @@ impl<'a> VimTestContext<'a> {
|
|||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
assertion_context: AssertionContextManager::new(),
|
||||
},
|
||||
workspace,
|
||||
}
|
||||
|
@ -120,18 +119,18 @@ impl<'a> VimTestContext<'a> {
|
|||
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
|
||||
self.cx.update(|cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(mode, false, cx);
|
||||
})
|
||||
});
|
||||
self.cx.set_state(text);
|
||||
self.cx.set_state(text)
|
||||
}
|
||||
|
||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||
self.assert_editor_state(text);
|
||||
assert_eq!(self.mode(), mode);
|
||||
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding<const COUNT: usize>(
|
||||
|
@ -145,8 +144,8 @@ impl<'a> VimTestContext<'a> {
|
|||
self.set_state(initial_state, initial_mode);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
assert_eq!(self.mode(), mode_after);
|
||||
assert_eq!(self.active_operator(), None);
|
||||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
|
@ -1,9 +0,0 @@
|
|||
mod neovim_backed_binding_test_context;
|
||||
mod neovim_backed_test_context;
|
||||
mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_binding_test_context::*;
|
||||
pub use vim_test_context::*;
|
|
@ -1,518 +0,0 @@
|
|||
use std::{
|
||||
ops::{Deref, DerefMut, Range},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use editor::DisplayPoint;
|
||||
use gpui::keymap::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
use super::{NeovimBackedBindingTestContext, VimTestContext};
|
||||
|
||||
pub struct NeovimBackedTestContext<'a> {
|
||||
cx: VimTestContext<'a>,
|
||||
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||
neovim: NeovimConnection,
|
||||
}
|
||||
|
||||
impl<'a> NeovimBackedTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||
let function_name = cx.function_name.clone();
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
Self {
|
||||
cx,
|
||||
exemptions: Default::default(),
|
||||
neovim: NeovimConnection::new(function_name).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
|
||||
let initial_state = initial_state.to_string();
|
||||
// None represents all keybindings being exempted for that initial state
|
||||
self.exemptions.insert(initial_state, None);
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
let special = keystroke.shift
|
||||
|| keystroke.ctrl
|
||||
|| keystroke.alt
|
||||
|| keystroke.cmd
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.ctrl { "C-" } else { "" };
|
||||
let alt = if keystroke.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.cmd { "D-" } else { "" };
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
||||
self.neovim
|
||||
.input(&key)
|
||||
.await
|
||||
.expect("Could not input keystroke");
|
||||
}
|
||||
|
||||
let window_id = self.window_id;
|
||||
self.cx.dispatch_keystroke(window_id, keystroke, false);
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_shared_keystroke(keystroke_text).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) {
|
||||
self.set_state(marked_text, Mode::Normal);
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
let cursor_point =
|
||||
self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||
let nvim_buffer = self
|
||||
.neovim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let lines = self
|
||||
.buffer_text()
|
||||
.split('\n')
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nvim_buffer
|
||||
.set_lines(0, -1, false, lines)
|
||||
.await
|
||||
.expect("Could not set nvim buffer text");
|
||||
|
||||
self.neovim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
self.neovim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
|
||||
let nvim_window = self
|
||||
.neovim
|
||||
.get_current_win()
|
||||
.await
|
||||
.expect("Could not get neovim window");
|
||||
nvim_window
|
||||
.set_cursor((
|
||||
cursor_point.head().row as i64 + 1,
|
||||
cursor_point.head().column as i64,
|
||||
))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
assert_eq!(
|
||||
self.neovim.text().await,
|
||||
self.buffer_text(),
|
||||
"{}",
|
||||
self.assertion_context.context()
|
||||
);
|
||||
|
||||
let zed_selection = self.update_editor(|editor, cx| editor.selections.newest_display(cx));
|
||||
let mut zed_selection_range = zed_selection.range();
|
||||
// Zed selections adjust themselves to make the end point visually make sense
|
||||
if zed_selection.reversed {
|
||||
*zed_selection_range.end.column_mut() =
|
||||
zed_selection_range.end.column().saturating_sub(1);
|
||||
}
|
||||
let neovim_selection = self.neovim.selection().await;
|
||||
assert_eq!(
|
||||
neovim_selection,
|
||||
zed_selection_range,
|
||||
"{}",
|
||||
self.assertion_context.context()
|
||||
);
|
||||
|
||||
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||
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),
|
||||
Selection { start: (u32, u32), end: (u32, 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");
|
||||
|
||||
// Makes system act a little more like zed in terms of indentation
|
||||
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||
.await
|
||||
.expect("Could not set smartindent on startup");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
data: Default::default(),
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
data: Self::read_test_data(&test_case_id),
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn text(&mut self) -> String {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let text = nvim_buffer
|
||||
.get_lines(0, -1, false)
|
||||
.await
|
||||
.expect("Could not get buffer text")
|
||||
.join("\n");
|
||||
|
||||
self.data.push_back(NeovimData::Text(text.clone()));
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn text(&mut self) -> String {
|
||||
if let Some(NeovimData::Text(text)) = self.data.pop_front() {
|
||||
text
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn selection(&mut self) -> Range<DisplayPoint> {
|
||||
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not exit visual mode");
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let (start_row, start_col) = nvim_buffer
|
||||
.get_mark("<")
|
||||
.await
|
||||
.expect("Could not get selection start");
|
||||
let (end_row, end_col) = nvim_buffer
|
||||
.get_mark(">")
|
||||
.await
|
||||
.expect("Could not get selection end");
|
||||
self.nvim
|
||||
.input("gv")
|
||||
.await
|
||||
.expect("Could not reselect visual selection");
|
||||
|
||||
(
|
||||
(start_row as u32 - 1, start_col as u32),
|
||||
(end_row as u32 - 1, end_col as u32),
|
||||
)
|
||||
} else {
|
||||
let nvim_row: u32 = self
|
||||
.nvim
|
||||
.command_output("echo line('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim rows start at 1
|
||||
let nvim_column: u32 = self
|
||||
.nvim
|
||||
.command_output("echo col('.')")
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
- 1; // Neovim columns start at 1
|
||||
|
||||
((nvim_row, nvim_column), (nvim_row, nvim_column))
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Selection { start, end });
|
||||
|
||||
DisplayPoint::new(start.0, start.1)..DisplayPoint::new(end.0, end.1)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn selection(&mut self) -> Range<DisplayPoint> {
|
||||
if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
|
||||
DisplayPoint::new(start.0, start.1)..DisplayPoint::new(end.0, end.1)
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
let nvim_mode_text = self
|
||||
.nvim
|
||||
.get_mode()
|
||||
.await
|
||||
.expect("Could not get mode")
|
||||
.into_iter()
|
||||
.find_map(|(key, value)| {
|
||||
if key.as_str() == Some("mode") {
|
||||
Some(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Could not find mode value");
|
||||
|
||||
let mode = match nvim_mode_text.as_ref() {
|
||||
"i" => Some(Mode::Insert),
|
||||
"n" => Some(Mode::Normal),
|
||||
"v" => Some(Mode::Visual { line: false }),
|
||||
"V" => Some(Mode::Visual { line: true }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.data.push_back(NeovimData::Mode(mode.clone()));
|
||||
|
||||
mode
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
|
||||
mode
|
||||
} else {
|
||||
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||
}
|
||||
}
|
||||
|
||||
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(format!("{}.json", test_case_id));
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let json = std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
);
|
||||
|
||||
serde_json::from_str(&json)
|
||||
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Deref for NeovimConnection {
|
||||
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl DerefMut for NeovimConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Drop for NeovimConnection {
|
||||
fn drop(&mut self) {
|
||||
let path = Self::test_data_path(&self.test_case_id);
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("Could not create test data directory");
|
||||
let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
|
||||
std::fs::write(path, json).expect("Could not write out test data");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[derive(Clone)]
|
||||
struct NvimHandler {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[async_trait]
|
||||
impl Handler for NvimHandler {
|
||||
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) -> Result<Value, Value> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn handle_notify(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::test_contexts::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.set_shared_state("This is a tesˇt").await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
#[cfg(test)]
|
||||
mod test_contexts;
|
||||
mod test;
|
||||
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
|
@ -231,101 +231,3 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate_shared_keystroke("i").await;
|
||||
cx.simulate_shared_keystrokes([
|
||||
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
|
||||
])
|
||||
.await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.assert_editor_state("ˇtest");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.blur());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.focus_self());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hˇjkl");
|
||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hTestˇjkl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("/");
|
||||
|
||||
// We now use a weird insert mode with selection when jumping to a single line editor
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ mod test {
|
|||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test_contexts::{NeovimBackedTestContext, VimTestContext},
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -305,20 +305,23 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["v", "w", "x"]);
|
||||
cx.assert("The quick ˇbrown").await;
|
||||
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["v", "w", "j", "x"],
|
||||
indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
the lazy dog"},
|
||||
)
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_shared_keystrokes(["j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
|
@ -370,147 +373,58 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert("The quick ˇbrown", "The quick ˇ");
|
||||
let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["v", "w", "c"]);
|
||||
cx.assert("The quick ˇbrown").await;
|
||||
let mut cx = cx.binding(["v", "w", "j", "c"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇhe lazy dog"},
|
||||
);
|
||||
let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["v", "b", "k", "c"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇuick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the ˇlazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"},
|
||||
);
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["shift-v", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on change
|
||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
fox jumps over
|
||||
ˇThe quick brown
|
||||
the lazy dog"});
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇ
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
ˇ"},
|
||||
);
|
||||
let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-v", "j", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
ˇ
|
||||
the lazy dog"},
|
||||
);
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
the lazy dog
|
||||
ˇThe quick brown
|
||||
fox jumps over"});
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇ"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the laˇzy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
ˇ"},
|
||||
);
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -619,7 +533,7 @@ mod test {
|
|||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumpsˇjumps over
|
||||
fox jumpsjumpˇs over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue