Use a more stable, readable serialization format for neovim-backed vim tests

This commit is contained in:
Max Brunsfeld 2023-03-21 18:06:17 -07:00
parent c1f53358ba
commit eaee5571a0
74 changed files with 7441 additions and 161 deletions

View file

@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
use language::{OffsetRangeExt, Point};
use language::OffsetRangeExt;
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@ -108,11 +108,7 @@ impl<'a> NeovimBackedTestContext<'a> {
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::<Point>(cx));
let text = self.buffer_text();
self.neovim.set_state(selection, &text).await;
self.neovim.set_state(marked_text).await;
context_handle
}

View file

@ -9,7 +9,7 @@ use async_trait::async_trait;
#[cfg(feature = "neovim")]
use gpui::keymap_matcher::Keystroke;
use language::{Point, Selection};
use language::Point;
#[cfg(feature = "neovim")]
use lazy_static::lazy_static;
@ -36,11 +36,11 @@ lazy_static! {
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum NeovimData {
Text(String),
Selection { start: (u32, u32), end: (u32, u32) },
Mode(Option<Mode>),
Put { state: String },
Key(String),
Get { state: String, mode: Option<Mode> },
}
pub struct NeovimConnection {
@ -117,18 +117,30 @@ impl NeovimConnection {
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
self.data
.push_back(NeovimData::Key(keystroke_text.to_string()));
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) {}
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
self.data.pop_front();
}
assert_eq!(
self.data.pop_front(),
Some(NeovimData::Key(keystroke_text.to_string())),
"operation does not match recorded script. re-record with --features=neovim"
);
}
#[cfg(feature = "neovim")]
pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
pub async fn set_state(&mut self, marked_text: &str) {
let (text, selection) = parse_state(&marked_text);
let nvim_buffer = self
.nvim
.get_current_buf()
@ -162,18 +174,41 @@ impl NeovimConnection {
if !selection.is_empty() {
panic!("Setting neovim state with non empty selection not yet supported");
}
let cursor = selection.head();
let cursor = selection.start;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
if let Some(NeovimData::Get { mode, state }) = self.data.back() {
if *mode == Some(Mode::Normal) && *state == marked_text {
return;
}
}
self.data.push_back(NeovimData::Put {
state: marked_text.to_string(),
})
}
#[cfg(not(feature = "neovim"))]
pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
pub async fn set_state(&mut self, marked_text: &str) {
if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
if *mode == Some(Mode::Normal) && *text == marked_text {
return;
}
self.data.pop_front();
}
assert_eq!(
self.data.pop_front(),
Some(NeovimData::Put {
state: marked_text.to_string()
}),
"operation does not match recorded script. re-record with --features=neovim"
);
}
#[cfg(feature = "neovim")]
pub async fn text(&mut self) -> String {
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
let nvim_buffer = self
.nvim
.get_current_buf()
@ -185,22 +220,6 @@ impl NeovimConnection {
.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('.')")
@ -218,7 +237,30 @@ impl NeovimConnection {
.unwrap()
- 1; // Neovim columns start at 1
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
let nvim_mode_text = self
.nvim
.get_mode()
.await
.expect("Could not get mode")
.into_iter()
.find_map(|(key, value)| {
if key.as_str() == Some("mode") {
Some(value.as_str().unwrap().to_owned())
} else {
None
}
})
.expect("Could not find mode value");
let mode = match nvim_mode_text.as_ref() {
"i" => Some(Mode::Insert),
"n" => Some(Mode::Normal),
"v" => Some(Mode::Visual { line: false }),
"V" => Some(Mode::Visual { line: true }),
_ => None,
};
let (start, end) = if let Some(Mode::Visual { .. }) = mode {
self.nvim
.input("<escape>")
.await
@ -243,72 +285,54 @@ impl NeovimConnection {
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),
Point::new(end_row as u32 - 1, end_col as u32),
Point::new(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),
Point::new(start_row as u32 - 1, start_col as u32),
Point::new(end_row as u32 - 1, end_col as u32),
)
}
} else {
((cursor_row, cursor_col), (cursor_row, cursor_col))
(
Point::new(cursor_row, cursor_col),
Point::new(cursor_row, cursor_col),
)
};
self.data.push_back(NeovimData::Selection { start, end });
let state = NeovimData::Get {
mode,
state: encode_range(&text, start..end),
};
Point::new(start.0, start.1)..Point::new(end.0, end.1)
if self.data.back() != Some(&state) {
self.data.push_back(state.clone());
}
(mode, text, start..end)
}
#[cfg(not(feature = "neovim"))]
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
let (text, range) = parse_state(text);
(*mode, text, range)
} else {
panic!("operation does not match recorded script. re-record with --features=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");
}
self.state().await.2
}
#[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
self.state().await.0
}
#[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");
}
pub async fn text(&mut self) -> String {
self.state().await.1
}
fn test_data_path(test_case_id: &str) -> PathBuf {
@ -325,8 +349,27 @@ impl NeovimConnection {
"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'")
let mut result = VecDeque::new();
for line in json.lines() {
result.push_back(
serde_json::from_str(line)
.expect("invalid test data. regenerate it with '--features neovim'"),
);
}
result
}
#[cfg(feature = "neovim")]
fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
let path = Self::test_data_path(test_case_id);
let mut json = Vec::new();
for entry in data {
serde_json::to_writer(&mut json, entry).unwrap();
json.push(b'\n');
}
std::fs::create_dir_all(path.parent().unwrap())
.expect("could not create test data directory");
std::fs::write(path, json).expect("could not write out test data");
}
}
@ -349,11 +392,7 @@ impl DerefMut for NeovimConnection {
#[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");
Self::write_test_data(&self.test_case_id, &self.data);
}
}
@ -383,3 +422,52 @@ impl Handler for NvimHandler {
) {
}
}
fn parse_state(marked_text: &str) -> (String, Range<Point>) {
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
let byte_range = ranges[0].clone();
let mut point_range = Point::zero()..Point::zero();
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if ix == byte_range.start {
point_range.start = position;
}
if ix == byte_range.end {
point_range.end = position;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
(text, point_range)
}
#[cfg(feature = "neovim")]
fn encode_range(text: &str, range: Range<Point>) -> String {
let mut byte_range = 0..0;
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if position == range.start {
byte_range.start = ix;
}
if position == range.end {
byte_range.end = ix;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
util::test::generate_marked_text(text, &[byte_range], true)
}