Use a more stable, readable serialization format for neovim-backed vim tests
This commit is contained in:
parent
c1f53358ba
commit
eaee5571a0
74 changed files with 7441 additions and 161 deletions
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue