From d2494822b006278d3c94c4cd63b676412deff232 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Mon, 10 Oct 2022 14:46:07 -0700 Subject: [PATCH] Add assertion context manager to TestAppContext and convert existing vim tests to use neovim backed test context --- Cargo.lock | 2 + crates/editor/src/editor_tests.rs | 14 +- .../editor/src/highlight_matching_bracket.rs | 3 +- crates/editor/src/hover_popover.rs | 4 +- crates/editor/src/link_go_to_definition.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 3 +- crates/editor/src/test.rs | 512 +------------- .../src/test/editor_lsp_test_context.rs | 208 ++++++ crates/editor/src/test/editor_test_context.rs | 273 ++++++++ crates/gpui/Cargo.toml | 1 + crates/gpui/src/app.rs | 634 +---------------- crates/gpui/src/app/test_app_context.rs | 655 ++++++++++++++++++ crates/vim/Cargo.toml | 1 + crates/vim/src/insert.rs | 2 +- crates/vim/src/normal.rs | 470 ++++--------- crates/vim/src/normal/change.rs | 2 +- crates/vim/src/normal/delete.rs | 2 +- crates/vim/src/object.rs | 2 +- crates/vim/src/test.rs | 102 +++ .../neovim_backed_binding_test_context.rs | 7 +- .../src/test/neovim_backed_test_context.rs | 158 +++++ crates/vim/src/test/neovim_connection.rs | 383 ++++++++++ .../vim_binding_test_context.rs | 0 .../vim_test_context.rs | 15 +- crates/vim/src/test_contexts.rs | 9 - .../neovim_backed_test_context.rs | 518 -------------- crates/vim/src/vim.rs | 100 +-- crates/vim/src/visual.rs | 178 ++--- crates/vim/test_data/test_b.json | 1 + crates/vim/test_data/test_cc.json | 1 + crates/vim/test_data/test_dd.json | 1 + crates/vim/test_data/test_delete_left.json | 1 + .../test_data/test_delete_to_end_of_line.json | 1 + .../vim/test_data/test_enter_visual_mode.json | 2 +- .../test_insert_first_non_whitespace.json | 1 + .../test_jump_to_first_non_whitespace.json | 1 + crates/vim/test_data/test_o.json | 1 + crates/vim/test_data/test_p.json | 1 + crates/vim/test_data/test_visual_change.json | 1 + .../test_data/test_visual_line_change.json | 1 + crates/vim/test_data/test_x.json | 1 + 41 files changed, 2062 insertions(+), 2212 deletions(-) create mode 100644 crates/editor/src/test/editor_lsp_test_context.rs create mode 100644 crates/editor/src/test/editor_test_context.rs create mode 100644 crates/gpui/src/app/test_app_context.rs create mode 100644 crates/vim/src/test.rs rename crates/vim/src/{test_contexts => test}/neovim_backed_binding_test_context.rs (92%) create mode 100644 crates/vim/src/test/neovim_backed_test_context.rs create mode 100644 crates/vim/src/test/neovim_connection.rs rename crates/vim/src/{test_contexts => test}/vim_binding_test_context.rs (100%) rename crates/vim/src/{test_contexts => test}/vim_test_context.rs (91%) delete mode 100644 crates/vim/src/test_contexts.rs delete mode 100644 crates/vim/src/test_contexts/neovim_backed_test_context.rs create mode 100644 crates/vim/test_data/test_b.json create mode 100644 crates/vim/test_data/test_cc.json create mode 100644 crates/vim/test_data/test_dd.json create mode 100644 crates/vim/test_data/test_delete_left.json create mode 100644 crates/vim/test_data/test_delete_to_end_of_line.json create mode 100644 crates/vim/test_data/test_insert_first_non_whitespace.json create mode 100644 crates/vim/test_data/test_jump_to_first_non_whitespace.json create mode 100644 crates/vim/test_data/test_o.json create mode 100644 crates/vim/test_data/test_p.json create mode 100644 crates/vim/test_data/test_visual_change.json create mode 100644 crates/vim/test_data/test_visual_line_change.json create mode 100644 crates/vim/test_data/test_x.json diff --git a/Cargo.lock b/Cargo.lock index 1da2348ff9..aa9ad80001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,6 +2420,7 @@ dependencies = [ "futures 0.3.24", "gpui_macros", "image", + "itertools", "lazy_static", "log", "media", @@ -6736,6 +6737,7 @@ dependencies = [ "indoc", "itertools", "language", + "lazy_static", "log", "nvim-rs", "parking_lot 0.11.2", diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 430b958407..d2d7a9dfef 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1,20 +1,22 @@ +use std::{cell::RefCell, rc::Rc, time::Instant}; + +use futures::StreamExt; +use indoc::indoc; +use unindent::Unindent; + use super::*; use crate::test::{ - assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext, - EditorTestContext, + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, }; -use futures::StreamExt; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; -use indoc::indoc; use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry}; use project::FakeFs; use settings::EditorSettings; -use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; -use unindent::Unindent; use util::{ assert_set_eq, test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 789393d70b..043b21db21 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon #[cfg(test)] mod tests { + use crate::test::editor_lsp_test_context::EditorLspTestContext; + use super::*; - use crate::test::EditorLspTestContext; use indoc::indoc; use language::{BracketPair, Language, LanguageConfig}; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 250f8427a5..38b28f0630 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -427,13 +427,13 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { - use futures::StreamExt; use indoc::indoc; use language::{Diagnostic, DiagnosticSet}; use project::HoverBlock; + use smol::stream::StreamExt; - use crate::test::EditorLspTestContext; + use crate::test::editor_lsp_test_context::EditorLspTestContext; use super::*; diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 6b23a04b67..c8294ddb43 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -400,7 +400,7 @@ mod tests { use indoc::indoc; use lsp::request::{GotoDefinition, GotoTypeDefinition}; - use crate::test::EditorLspTestContext; + use crate::test::editor_lsp_test_context::EditorLspTestContext; use super::*; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 4adc030d99..d9840fd3fa 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -70,8 +70,9 @@ pub fn deploy_context_menu( #[cfg(test)] mod tests { + use crate::test::editor_lsp_test_context::EditorLspTestContext; + use super::*; - use crate::test::EditorLspTestContext; use indoc::indoc; #[gpui::test] diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index acee1216d1..48652c44b7 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,34 +1,14 @@ +pub mod editor_lsp_test_context; +pub mod editor_test_context; + use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - multi_buffer::ToPointUtf16, - AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint, + DisplayPoint, Editor, EditorMode, MultiBuffer, }; -use anyhow::Result; -use collections::BTreeMap; -use futures::{Future, StreamExt}; -use gpui::{ - json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle, -}; -use indoc::indoc; -use itertools::Itertools; -use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig}; -use lsp::{notification, request}; -use parking_lot::RwLock; -use project::Project; -use settings::Settings; -use std::{ - any::TypeId, - ops::{Deref, DerefMut, Range}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, -}; -use util::{ - assert_set_eq, set_eq, - test::{generate_marked_text, marked_text_offsets, marked_text_ranges}, -}; -use workspace::{pane, AppState, Workspace, WorkspaceHandle}; + +use gpui::{ModelHandle, ViewContext}; + +use util::test::{marked_text_offsets, marked_text_ranges}; #[cfg(test)] #[ctor::ctor] @@ -86,479 +66,3 @@ pub(crate) fn build_editor( ) -> Editor { Editor::new(EditorMode::Full, buffer, None, None, cx) } - -pub struct EditorTestContext<'a> { - pub cx: &'a mut gpui::TestAppContext, - pub window_id: usize, - pub editor: ViewHandle, - pub assertion_context: AssertionContextManager, -} - -impl<'a> EditorTestContext<'a> { - pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { - let (window_id, editor) = cx.update(|cx| { - cx.set_global(Settings::test(cx)); - crate::init(cx); - - let (window_id, editor) = cx.add_window(Default::default(), |cx| { - build_editor(MultiBuffer::build_simple("", cx), cx) - }); - - editor.update(cx, |_, cx| cx.focus_self()); - - (window_id, editor) - }); - - Self { - cx, - window_id, - editor, - assertion_context: AssertionContextManager::new(), - } - } - - pub fn add_assertion_context(&self, context: String) -> ContextHandle { - self.assertion_context.add_context(context) - } - - pub fn condition( - &self, - predicate: impl FnMut(&Editor, &AppContext) -> bool, - ) -> impl Future { - self.editor.condition(self.cx, predicate) - } - - pub fn editor(&self, read: F) -> T - where - F: FnOnce(&Editor, &AppContext) -> T, - { - self.editor.read_with(self.cx, read) - } - - pub fn update_editor(&mut self, update: F) -> T - where - F: FnOnce(&mut Editor, &mut ViewContext) -> T, - { - self.editor.update(self.cx, update) - } - - pub fn multibuffer(&self, read: F) -> T - where - F: FnOnce(&MultiBuffer, &AppContext) -> T, - { - self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) - } - - pub fn update_multibuffer(&mut self, update: F) -> T - where - F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, - { - self.update_editor(|editor, cx| editor.buffer().update(cx, update)) - } - - pub fn buffer_text(&self) -> String { - self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) - } - - pub fn buffer(&self, read: F) -> T - where - F: FnOnce(&Buffer, &AppContext) -> T, - { - self.multibuffer(|multibuffer, cx| { - let buffer = multibuffer.as_singleton().unwrap().read(cx); - read(buffer, cx) - }) - } - - pub fn update_buffer(&mut self, update: F) -> T - where - F: FnOnce(&mut Buffer, &mut ModelContext) -> T, - { - self.update_multibuffer(|multibuffer, cx| { - let buffer = multibuffer.as_singleton().unwrap(); - buffer.update(cx, update) - }) - } - - pub fn buffer_snapshot(&self) -> BufferSnapshot { - self.buffer(|buffer, _| buffer.snapshot()) - } - - pub fn simulate_keystroke(&mut self, keystroke_text: &str) { - let keystroke = Keystroke::parse(keystroke_text).unwrap(); - self.cx.dispatch_keystroke(self.window_id, keystroke, false); - } - - pub fn simulate_keystrokes(&mut self, keystroke_texts: [&str; COUNT]) { - for keystroke_text in keystroke_texts.into_iter() { - self.simulate_keystroke(keystroke_text); - } - } - - pub fn ranges(&self, marked_text: &str) -> Vec> { - let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); - assert_eq!(self.buffer_text(), unmarked_text); - ranges - } - - pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { - let ranges = self.ranges(marked_text); - let snapshot = self - .editor - .update(self.cx, |editor, cx| editor.snapshot(cx)); - ranges[0].start.to_display_point(&snapshot) - } - - // Returns anchors for the current buffer using `«` and `»` - pub fn text_anchor_range(&self, marked_text: &str) -> Range { - let ranges = self.ranges(marked_text); - let snapshot = self.buffer_snapshot(); - snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) - } - - /// Change the editor's text and selections using a string containing - /// embedded range markers that represent the ranges and directions of - /// each selection. - /// - /// See the `util::test::marked_text_ranges` function for more information. - pub fn set_state(&mut self, marked_text: &str) { - let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { - editor.set_text(unmarked_text, cx); - editor.change_selections(Some(Autoscroll::Fit), cx, |s| { - s.select_ranges(selection_ranges) - }) - }) - } - - /// Make an assertion about the editor's text and the ranges and directions - /// of its selections using a string containing embedded range markers. - /// - /// See the `util::test::marked_text_ranges` function for more information. - pub fn assert_editor_state(&mut self, marked_text: &str) { - let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); - let buffer_text = self.buffer_text(); - assert_eq!( - buffer_text, unmarked_text, - "Unmarked text doesn't match buffer text" - ); - self.assert_selections(expected_selections, marked_text.to_string()) - } - - pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { - let expected_ranges = self.ranges(marked_text); - let actual_ranges: Vec> = self.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); - editor - .background_highlights - .get(&TypeId::of::()) - .map(|h| h.1.clone()) - .unwrap_or_default() - .into_iter() - .map(|range| range.to_offset(&snapshot.buffer_snapshot)) - .collect() - }); - assert_set_eq!(actual_ranges, expected_ranges); - } - - pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { - let expected_ranges = self.ranges(marked_text); - let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); - let actual_ranges: Vec> = snapshot - .highlight_ranges::() - .map(|ranges| ranges.as_ref().clone().1) - .unwrap_or_default() - .into_iter() - .map(|range| range.to_offset(&snapshot.buffer_snapshot)) - .collect(); - assert_set_eq!(actual_ranges, expected_ranges); - } - - pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { - let expected_marked_text = - generate_marked_text(&self.buffer_text(), &expected_selections, true); - self.assert_selections(expected_selections, expected_marked_text) - } - - fn assert_selections( - &mut self, - expected_selections: Vec>, - expected_marked_text: String, - ) { - let actual_selections = self - .editor - .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) - .into_iter() - .map(|s| { - if s.reversed { - s.end..s.start - } else { - s.start..s.end - } - }) - .collect::>(); - let actual_marked_text = - generate_marked_text(&self.buffer_text(), &actual_selections, true); - if expected_selections != actual_selections { - panic!( - indoc! {" - Editor has unexpected selections. - - Expected selections: - {} - - Actual selections: - {} - "}, - expected_marked_text, actual_marked_text, - ); - } - } -} - -impl<'a> Deref for EditorTestContext<'a> { - type Target = gpui::TestAppContext; - - fn deref(&self) -> &Self::Target { - self.cx - } -} - -impl<'a> DerefMut for EditorTestContext<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} - -pub struct EditorLspTestContext<'a> { - pub cx: EditorTestContext<'a>, - pub lsp: lsp::FakeLanguageServer, - pub workspace: ViewHandle, - pub buffer_lsp_url: lsp::Url, -} - -impl<'a> EditorLspTestContext<'a> { - pub async fn new( - mut language: Language, - capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { - use json::json; - - cx.update(|cx| { - crate::init(cx); - pane::init(cx); - }); - - let params = cx.update(AppState::test); - - let file_name = format!( - "file.{}", - language - .path_suffixes() - .first() - .unwrap_or(&"txt".to_string()) - ); - - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities, - ..Default::default() - })) - .await; - - let project = Project::test(params.fs.clone(), [], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - - params - .fs - .as_fake() - .insert_tree("/root", json!({ "dir": { file_name: "" }})) - .await; - - let (window_id, workspace) = - cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); - project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root", true, cx) - }) - .await - .unwrap(); - cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) - .await; - - let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); - let item = workspace - .update(cx, |workspace, cx| workspace.open_path(file, true, cx)) - .await - .expect("Could not open test file"); - - let editor = cx.update(|cx| { - item.act_as::(cx) - .expect("Opened test file wasn't an editor") - }); - editor.update(cx, |_, cx| cx.focus_self()); - - let lsp = fake_servers.next().await.unwrap(); - - Self { - cx: EditorTestContext { - cx, - window_id, - editor, - assertion_context: AssertionContextManager::new(), - }, - lsp, - workspace, - buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), - } - } - - pub async fn new_rust( - capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { - let language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - - Self::new(language, capabilities, cx).await - } - - // Constructs lsp range using a marked string with '[', ']' range delimiters - pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { - let ranges = self.ranges(marked_text); - self.to_lsp_range(ranges[0].clone()) - } - - pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { - let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); - let start_point = range.start.to_point(&snapshot.buffer_snapshot); - let end_point = range.end.to_point(&snapshot.buffer_snapshot); - - self.editor(|editor, cx| { - let buffer = editor.buffer().read(cx); - let start = point_to_lsp( - buffer - .point_to_buffer_offset(start_point, cx) - .unwrap() - .1 - .to_point_utf16(&buffer.read(cx)), - ); - let end = point_to_lsp( - buffer - .point_to_buffer_offset(end_point, cx) - .unwrap() - .1 - .to_point_utf16(&buffer.read(cx)), - ); - - lsp::Range { start, end } - }) - } - - pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { - let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); - let point = offset.to_point(&snapshot.buffer_snapshot); - - self.editor(|editor, cx| { - let buffer = editor.buffer().read(cx); - point_to_lsp( - buffer - .point_to_buffer_offset(point, cx) - .unwrap() - .1 - .to_point_utf16(&buffer.read(cx)), - ) - }) - } - - pub fn update_workspace(&mut self, update: F) -> T - where - F: FnOnce(&mut Workspace, &mut ViewContext) -> T, - { - self.workspace.update(self.cx.cx, update) - } - - pub fn handle_request( - &self, - mut handler: F, - ) -> futures::channel::mpsc::UnboundedReceiver<()> - where - T: 'static + request::Request, - T::Params: 'static + Send, - F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, - Fut: 'static + Send + Future>, - { - let url = self.buffer_lsp_url.clone(); - self.lsp.handle_request::(move |params, cx| { - let url = url.clone(); - handler(url, params, cx) - }) - } - - pub fn notify(&self, params: T::Params) { - self.lsp.notify::(params); - } -} - -impl<'a> Deref for EditorLspTestContext<'a> { - type Target = EditorTestContext<'a>; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl<'a> DerefMut for EditorLspTestContext<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} - -#[derive(Clone)] -pub struct AssertionContextManager { - id: Arc, - contexts: Arc>>, -} - -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); - } -} diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs new file mode 100644 index 0000000000..b4a4cd7ab8 --- /dev/null +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -0,0 +1,208 @@ +use std::{ + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; + +use anyhow::Result; + +use futures::Future; +use gpui::{json, ViewContext, ViewHandle}; +use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig}; +use lsp::{notification, request}; +use project::Project; +use smol::stream::StreamExt; +use workspace::{pane, AppState, Workspace, WorkspaceHandle}; + +use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint}; + +use super::editor_test_context::EditorTestContext; + +pub struct EditorLspTestContext<'a> { + pub cx: EditorTestContext<'a>, + pub lsp: lsp::FakeLanguageServer, + pub workspace: ViewHandle, + pub buffer_lsp_url: lsp::Url, +} + +impl<'a> EditorLspTestContext<'a> { + pub async fn new( + mut language: Language, + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + use json::json; + + cx.update(|cx| { + crate::init(cx); + pane::init(cx); + }); + + let params = cx.update(AppState::test); + + let file_name = format!( + "file.{}", + language + .path_suffixes() + .first() + .unwrap_or(&"txt".to_string()) + ); + + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities, + ..Default::default() + })) + .await; + + let project = Project::test(params.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + params + .fs + .as_fake() + .insert_tree("/root", json!({ "dir": { file_name: "" }})) + .await; + + let (window_id, workspace) = + cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| workspace.open_path(file, true, cx)) + .await + .expect("Could not open test file"); + + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); + + let lsp = fake_servers.next().await.unwrap(); + + Self { + cx: EditorTestContext { + cx, + window_id, + editor, + }, + lsp, + workspace, + buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + } + } + + pub async fn new_rust( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + Self::new(language, capabilities, cx).await + } + + // Constructs lsp range using a marked string with '[', ']' range delimiters + pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { + let ranges = self.ranges(marked_text); + self.to_lsp_range(ranges[0].clone()) + } + + pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let start_point = range.start.to_point(&snapshot.buffer_snapshot); + let end_point = range.end.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + let start = point_to_lsp( + buffer + .point_to_buffer_offset(start_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + let end = point_to_lsp( + buffer + .point_to_buffer_offset(end_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + + lsp::Range { start, end } + }) + } + + pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let point = offset.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + point_to_lsp( + buffer + .point_to_buffer_offset(point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ) + }) + } + + pub fn update_workspace(&mut self, update: F) -> T + where + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + self.workspace.update(self.cx.cx, update) + } + + pub fn handle_request( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future>, + { + let url = self.buffer_lsp_url.clone(); + self.lsp.handle_request::(move |params, cx| { + let url = url.clone(); + handler(url, params, cx) + }) + } + + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } +} + +impl<'a> Deref for EditorLspTestContext<'a> { + type Target = EditorTestContext<'a>; + + fn deref(&self) -> &Self::Target { + &self.cx + } +} + +impl<'a> DerefMut for EditorLspTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs new file mode 100644 index 0000000000..73dc6bfd6e --- /dev/null +++ b/crates/editor/src/test/editor_test_context.rs @@ -0,0 +1,273 @@ +use std::{ + any::TypeId, + ops::{Deref, DerefMut, Range}, +}; + +use futures::Future; +use indoc::indoc; + +use crate::{ + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, +}; +use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle}; +use language::{Buffer, BufferSnapshot}; +use settings::Settings; +use util::{ + assert_set_eq, + test::{generate_marked_text, marked_text_ranges}, +}; + +use super::build_editor; + +pub struct EditorTestContext<'a> { + pub cx: &'a mut gpui::TestAppContext, + pub window_id: usize, + pub editor: ViewHandle, +} + +impl<'a> EditorTestContext<'a> { + pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { + let (window_id, editor) = cx.update(|cx| { + cx.set_global(Settings::test(cx)); + crate::init(cx); + + let (window_id, editor) = cx.add_window(Default::default(), |cx| { + build_editor(MultiBuffer::build_simple("", cx), cx) + }); + + editor.update(cx, |_, cx| cx.focus_self()); + + (window_id, editor) + }); + + Self { + cx, + window_id, + editor, + } + } + + pub fn condition( + &self, + predicate: impl FnMut(&Editor, &AppContext) -> bool, + ) -> impl Future { + self.editor.condition(self.cx, predicate) + } + + pub fn editor(&self, read: F) -> T + where + F: FnOnce(&Editor, &AppContext) -> T, + { + self.editor.read_with(self.cx, read) + } + + pub fn update_editor(&mut self, update: F) -> T + where + F: FnOnce(&mut Editor, &mut ViewContext) -> T, + { + self.editor.update(self.cx, update) + } + + pub fn multibuffer(&self, read: F) -> T + where + F: FnOnce(&MultiBuffer, &AppContext) -> T, + { + self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) + } + + pub fn update_multibuffer(&mut self, update: F) -> T + where + F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, + { + self.update_editor(|editor, cx| editor.buffer().update(cx, update)) + } + + pub fn buffer_text(&self) -> String { + self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) + } + + pub fn buffer(&self, read: F) -> T + where + F: FnOnce(&Buffer, &AppContext) -> T, + { + self.multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap().read(cx); + read(buffer, cx) + }) + } + + pub fn update_buffer(&mut self, update: F) -> T + where + F: FnOnce(&mut Buffer, &mut ModelContext) -> T, + { + self.update_multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + buffer.update(cx, update) + }) + } + + pub fn buffer_snapshot(&self) -> BufferSnapshot { + self.buffer(|buffer, _| buffer.snapshot()) + } + + pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { + let keystroke_under_test_handle = + self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + self.cx.dispatch_keystroke(self.window_id, keystroke, false); + keystroke_under_test_handle + } + + pub fn simulate_keystrokes( + &mut self, + keystroke_texts: [&str; COUNT], + ) -> ContextHandle { + let keystrokes_under_test_handle = + self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + keystrokes_under_test_handle + } + + pub fn ranges(&self, marked_text: &str) -> Vec> { + let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); + assert_eq!(self.buffer_text(), unmarked_text); + ranges + } + + pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { + let ranges = self.ranges(marked_text); + let snapshot = self + .editor + .update(self.cx, |editor, cx| editor.snapshot(cx)); + ranges[0].start.to_display_point(&snapshot) + } + + // Returns anchors for the current buffer using `«` and `»` + pub fn text_anchor_range(&self, marked_text: &str) -> Range { + let ranges = self.ranges(marked_text); + let snapshot = self.buffer_snapshot(); + snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) + } + + /// Change the editor's text and selections using a string containing + /// embedded range markers that represent the ranges and directions of + /// each selection. + /// + /// See the `util::test::marked_text_ranges` function for more information. + pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { + let _state_context = self.add_assertion_context(format!( + "Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(self.cx, |editor, cx| { + editor.set_text(unmarked_text, cx); + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + _state_context + } + + /// Make an assertion about the editor's text and the ranges and directions + /// of its selections using a string containing embedded range markers. + /// + /// See the `util::test::marked_text_ranges` function for more information. + pub fn assert_editor_state(&mut self, marked_text: &str) { + let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); + let buffer_text = self.buffer_text(); + assert_eq!( + buffer_text, unmarked_text, + "Unmarked text doesn't match buffer text" + ); + self.assert_selections(expected_selections, marked_text.to_string()) + } + + pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let actual_ranges: Vec> = self.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .background_highlights + .get(&TypeId::of::()) + .map(|h| h.1.clone()) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect() + }); + assert_set_eq!(actual_ranges, expected_ranges); + } + + pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let actual_ranges: Vec> = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect(); + assert_set_eq!(actual_ranges, expected_ranges); + } + + pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { + let expected_marked_text = + generate_marked_text(&self.buffer_text(), &expected_selections, true); + self.assert_selections(expected_selections, expected_marked_text) + } + + fn assert_selections( + &mut self, + expected_selections: Vec>, + expected_marked_text: String, + ) { + let actual_selections = self + .editor + .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + .into_iter() + .map(|s| { + if s.reversed { + s.end..s.start + } else { + s.start..s.end + } + }) + .collect::>(); + let actual_marked_text = + generate_marked_text(&self.buffer_text(), &actual_selections, true); + if expected_selections != actual_selections { + panic!( + indoc! {" + {}Editor has unexpected selections. + + Expected selections: + {} + + Actual selections: + {} + "}, + self.assertion_context(), + expected_marked_text, + actual_marked_text, + ); + } + } +} + +impl<'a> Deref for EditorTestContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} + +impl<'a> DerefMut for EditorTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 51bc416e19..54fe5e46a2 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -25,6 +25,7 @@ env_logger = { version = "0.9", optional = true } etagere = "0.2" futures = "0.3" image = "0.23" +itertools = "0.10" lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } num_cpus = "1.13" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1da9f35dc8..16c0a3bae4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,28 +1,8 @@ pub mod action; mod callback_collection; +#[cfg(any(test, feature = "test-support"))] +pub mod test_app_context; -use crate::{ - elements::ElementBox, - executor::{self, Task}, - geometry::rect::RectF, - keymap::{self, Binding, Keystroke}, - platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, - presenter::Presenter, - util::post_inc, - Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, - MouseRegionId, PathPromptOptions, TextLayoutCache, -}; -pub use action::*; -use anyhow::{anyhow, Context, Result}; -use callback_collection::CallbackCollection; -use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}; -use keymap::MatchResult; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use platform::Event; -use postage::oneshot; -use smallvec::SmallVec; -use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, cell::RefCell, @@ -38,7 +18,32 @@ use std::{ time::Duration, }; -use self::callback_collection::Mapping; +use anyhow::{anyhow, Context, Result}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use postage::oneshot; +use smallvec::SmallVec; +use smol::prelude::*; + +pub use action::*; +use callback_collection::{CallbackCollection, Mapping}; +use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}; +use keymap::MatchResult; +use platform::Event; +#[cfg(any(test, feature = "test-support"))] +pub use test_app_context::{ContextHandle, TestAppContext}; + +use crate::{ + elements::ElementBox, + executor::{self, Task}, + geometry::rect::RectF, + keymap::{self, Binding, Keystroke}, + platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, + presenter::Presenter, + util::post_inc, + Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, + MouseRegionId, PathPromptOptions, TextLayoutCache, +}; pub trait Entity: 'static { type Event; @@ -177,14 +182,6 @@ pub struct App(Rc>); #[derive(Clone)] pub struct AsyncAppContext(Rc>); -#[cfg(any(test, feature = "test-support"))] -pub struct TestAppContext { - cx: Rc>, - foreground_platform: Rc, - condition_duration: Option, - pub function_name: String, -} - pub struct WindowInputHandler { app: Rc>, window_id: usize, @@ -428,329 +425,6 @@ impl InputHandler for WindowInputHandler { } } -#[cfg(any(test, feature = "test-support"))] -impl TestAppContext { - pub fn new( - foreground_platform: Rc, - platform: Arc, - foreground: Rc, - background: Arc, - font_cache: Arc, - leak_detector: Arc>, - first_entity_id: usize, - function_name: String, - ) -> Self { - let mut cx = MutableAppContext::new( - foreground, - background, - platform, - foreground_platform.clone(), - font_cache, - RefCounts { - #[cfg(any(test, feature = "test-support"))] - leak_detector, - ..Default::default() - }, - (), - ); - cx.next_entity_id = first_entity_id; - let cx = TestAppContext { - cx: Rc::new(RefCell::new(cx)), - foreground_platform, - condition_duration: None, - function_name, - }; - cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx)); - cx - } - - pub fn dispatch_action(&self, window_id: usize, action: A) { - let mut cx = self.cx.borrow_mut(); - if let Some(view_id) = cx.focused_view_id(window_id) { - cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action); - } - } - - pub fn dispatch_global_action(&self, action: A) { - self.cx.borrow_mut().dispatch_global_action(action); - } - - pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { - let handled = self.cx.borrow_mut().update(|cx| { - let presenter = cx - .presenters_and_platform_windows - .get(&window_id) - .unwrap() - .0 - .clone(); - - if cx.dispatch_keystroke(window_id, &keystroke) { - return true; - } - - if presenter.borrow_mut().dispatch_event( - Event::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held, - }), - false, - cx, - ) { - return true; - } - - false - }); - - if !handled && !keystroke.cmd && !keystroke.ctrl { - WindowInputHandler { - app: self.cx.clone(), - window_id, - } - .replace_text_in_range(None, &keystroke.key) - } - } - - pub fn add_model(&mut self, build_model: F) -> ModelHandle - where - T: Entity, - F: FnOnce(&mut ModelContext) -> T, - { - self.cx.borrow_mut().add_model(build_model) - } - - pub fn add_window(&mut self, build_root_view: F) -> (usize, ViewHandle) - where - T: View, - F: FnOnce(&mut ViewContext) -> T, - { - let (window_id, view) = self - .cx - .borrow_mut() - .add_window(Default::default(), build_root_view); - self.simulate_window_activation(Some(window_id)); - (window_id, view) - } - - pub fn add_view( - &mut self, - parent_handle: impl Into, - build_view: F, - ) -> ViewHandle - where - T: View, - F: FnOnce(&mut ViewContext) -> T, - { - self.cx.borrow_mut().add_view(parent_handle, build_view) - } - - pub fn window_ids(&self) -> Vec { - self.cx.borrow().window_ids().collect() - } - - pub fn root_view(&self, window_id: usize) -> Option> { - self.cx.borrow().root_view(window_id) - } - - pub fn read T>(&self, callback: F) -> T { - callback(self.cx.borrow().as_ref()) - } - - pub fn update T>(&mut self, callback: F) -> T { - let mut state = self.cx.borrow_mut(); - // Don't increment pending flushes in order for effects to be flushed before the callback - // completes, which is helpful in tests. - let result = callback(&mut *state); - // Flush effects after the callback just in case there are any. This can happen in edge - // cases such as the closure dropping handles. - state.flush_effects(); - result - } - - pub fn render(&mut self, handle: &ViewHandle, f: F) -> T - where - F: FnOnce(&mut V, &mut RenderContext) -> T, - V: View, - { - handle.update(&mut *self.cx.borrow_mut(), |view, cx| { - let mut render_cx = RenderContext { - app: cx, - window_id: handle.window_id(), - view_id: handle.id(), - view_type: PhantomData, - titlebar_height: 0., - hovered_region_ids: Default::default(), - clicked_region_ids: None, - refreshing: false, - appearance: Appearance::Light, - }; - f(view, &mut render_cx) - }) - } - - pub fn to_async(&self) -> AsyncAppContext { - AsyncAppContext(self.cx.clone()) - } - - pub fn font_cache(&self) -> Arc { - self.cx.borrow().cx.font_cache.clone() - } - - pub fn foreground_platform(&self) -> Rc { - self.foreground_platform.clone() - } - - pub fn platform(&self) -> Arc { - self.cx.borrow().cx.platform.clone() - } - - pub fn foreground(&self) -> Rc { - self.cx.borrow().foreground().clone() - } - - pub fn background(&self) -> Arc { - self.cx.borrow().background().clone() - } - - pub fn spawn(&self, f: F) -> Task - where - F: FnOnce(AsyncAppContext) -> Fut, - Fut: 'static + Future, - T: 'static, - { - let foreground = self.foreground(); - let future = f(self.to_async()); - let cx = self.to_async(); - foreground.spawn(async move { - let result = future.await; - cx.0.borrow_mut().flush_effects(); - result - }) - } - - pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option) { - self.foreground_platform.simulate_new_path_selection(result); - } - - pub fn did_prompt_for_new_path(&self) -> bool { - self.foreground_platform.as_ref().did_prompt_for_new_path() - } - - pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) { - use postage::prelude::Sink as _; - - let mut done_tx = self - .window_mut(window_id) - .pending_prompts - .borrow_mut() - .pop_front() - .expect("prompt was not called"); - let _ = done_tx.try_send(answer); - } - - pub fn has_pending_prompt(&self, window_id: usize) -> bool { - let window = self.window_mut(window_id); - let prompts = window.pending_prompts.borrow_mut(); - !prompts.is_empty() - } - - pub fn current_window_title(&self, window_id: usize) -> Option { - self.window_mut(window_id).title.clone() - } - - pub fn simulate_window_close(&self, window_id: usize) -> bool { - let handler = self.window_mut(window_id).should_close_handler.take(); - if let Some(mut handler) = handler { - let should_close = handler(); - self.window_mut(window_id).should_close_handler = Some(handler); - should_close - } else { - false - } - } - - pub fn simulate_window_activation(&self, to_activate: Option) { - let mut handlers = BTreeMap::new(); - { - let mut cx = self.cx.borrow_mut(); - for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows { - let window = window - .as_any_mut() - .downcast_mut::() - .unwrap(); - handlers.insert( - *window_id, - mem::take(&mut window.active_status_change_handlers), - ); - } - }; - let mut handlers = handlers.into_iter().collect::>(); - handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate); - - for (window_id, mut window_handlers) in handlers { - for window_handler in &mut window_handlers { - window_handler(Some(window_id) == to_activate); - } - - self.window_mut(window_id) - .active_status_change_handlers - .extend(window_handlers); - } - } - - pub fn is_window_edited(&self, window_id: usize) -> bool { - self.window_mut(window_id).edited - } - - pub fn leak_detector(&self) -> Arc> { - self.cx.borrow().leak_detector() - } - - pub fn assert_dropped(&self, handle: impl WeakHandle) { - self.cx - .borrow() - .leak_detector() - .lock() - .assert_dropped(handle.id()) - } - - fn window_mut(&self, window_id: usize) -> std::cell::RefMut { - std::cell::RefMut::map(self.cx.borrow_mut(), |state| { - let (_, window) = state - .presenters_and_platform_windows - .get_mut(&window_id) - .unwrap(); - let test_window = window - .as_any_mut() - .downcast_mut::() - .unwrap(); - test_window - }) - } - - pub fn set_condition_duration(&mut self, duration: Option) { - self.condition_duration = duration; - } - - pub fn condition_duration(&self) -> Duration { - self.condition_duration.unwrap_or_else(|| { - if std::env::var("CI").is_ok() { - Duration::from_secs(2) - } else { - Duration::from_millis(500) - } - }) - } - - pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { - self.update(|cx| { - let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); - let expected_content = expected_content.map(|content| content.to_owned()); - assert_eq!(actual_content, expected_content); - }) - } -} - impl AsyncAppContext { pub fn spawn(&self, f: F) -> Task where @@ -879,60 +553,6 @@ impl ReadViewWith for AsyncAppContext { } } -#[cfg(any(test, feature = "test-support"))] -impl UpdateModel for TestAppContext { - fn update_model( - &mut self, - handle: &ModelHandle, - update: &mut dyn FnMut(&mut T, &mut ModelContext) -> O, - ) -> O { - self.cx.borrow_mut().update_model(handle, update) - } -} - -#[cfg(any(test, feature = "test-support"))] -impl ReadModelWith for TestAppContext { - fn read_model_with( - &self, - handle: &ModelHandle, - read: &mut dyn FnMut(&E, &AppContext) -> T, - ) -> T { - let cx = self.cx.borrow(); - let cx = cx.as_ref(); - read(handle.read(cx), cx) - } -} - -#[cfg(any(test, feature = "test-support"))] -impl UpdateView for TestAppContext { - fn update_view( - &mut self, - handle: &ViewHandle, - update: &mut dyn FnMut(&mut T, &mut ViewContext) -> S, - ) -> S - where - T: View, - { - self.cx.borrow_mut().update_view(handle, update) - } -} - -#[cfg(any(test, feature = "test-support"))] -impl ReadViewWith for TestAppContext { - fn read_view_with( - &self, - handle: &ViewHandle, - read: &mut dyn FnMut(&V, &AppContext) -> T, - ) -> T - where - V: View, - { - let cx = self.cx.borrow(); - let cx = cx.as_ref(); - read(handle.read(cx), cx) - } -} - type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize); type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext); @@ -4412,117 +4032,6 @@ impl ModelHandle { update(model, cx) }) } - - #[cfg(any(test, feature = "test-support"))] - pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { - let (tx, mut rx) = futures::channel::mpsc::unbounded(); - let mut cx = cx.cx.borrow_mut(); - let subscription = cx.observe(self, move |_, _| { - tx.unbounded_send(()).ok(); - }); - - let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(5) - } else { - Duration::from_secs(1) - }; - - async move { - let notification = crate::util::timeout(duration, rx.next()) - .await - .expect("next notification timed out"); - drop(subscription); - notification.expect("model dropped while test was waiting for its next notification") - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn next_event(&self, cx: &TestAppContext) -> impl Future - where - T::Event: Clone, - { - let (tx, mut rx) = futures::channel::mpsc::unbounded(); - let mut cx = cx.cx.borrow_mut(); - let subscription = cx.subscribe(self, move |_, event, _| { - tx.unbounded_send(event.clone()).ok(); - }); - - let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(5) - } else { - Duration::from_secs(1) - }; - - cx.foreground.start_waiting(); - async move { - let event = crate::util::timeout(duration, rx.next()) - .await - .expect("next event timed out"); - drop(subscription); - event.expect("model dropped while test was waiting for its next event") - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn condition( - &self, - cx: &TestAppContext, - mut predicate: impl FnMut(&T, &AppContext) -> bool, - ) -> impl Future { - let (tx, mut rx) = futures::channel::mpsc::unbounded(); - - let mut cx = cx.cx.borrow_mut(); - let subscriptions = ( - cx.observe(self, { - let tx = tx.clone(); - move |_, _| { - tx.unbounded_send(()).ok(); - } - }), - cx.subscribe(self, { - move |_, _, _| { - tx.unbounded_send(()).ok(); - } - }), - ); - - let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); - let handle = self.downgrade(); - let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(5) - } else { - Duration::from_secs(1) - }; - - async move { - crate::util::timeout(duration, async move { - loop { - { - let cx = cx.borrow(); - let cx = cx.as_ref(); - if predicate( - handle - .upgrade(cx) - .expect("model dropped with pending condition") - .read(cx), - cx, - ) { - break; - } - } - - cx.borrow().foreground().start_waiting(); - rx.next() - .await - .expect("model dropped with pending condition"); - cx.borrow().foreground().finish_waiting(); - } - }) - .await - .expect("condition timed out"); - drop(subscriptions); - } - } } impl Clone for ModelHandle { @@ -4749,93 +4258,6 @@ impl ViewHandle { cx.focused_view_id(self.window_id) .map_or(false, |focused_id| focused_id == self.view_id) } - - #[cfg(any(test, feature = "test-support"))] - pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { - use postage::prelude::{Sink as _, Stream as _}; - - let (mut tx, mut rx) = postage::mpsc::channel(1); - let mut cx = cx.cx.borrow_mut(); - let subscription = cx.observe(self, move |_, _| { - tx.try_send(()).ok(); - }); - - let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(5) - } else { - Duration::from_secs(1) - }; - - async move { - let notification = crate::util::timeout(duration, rx.recv()) - .await - .expect("next notification timed out"); - drop(subscription); - notification.expect("model dropped while test was waiting for its next notification") - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn condition( - &self, - cx: &TestAppContext, - mut predicate: impl FnMut(&T, &AppContext) -> bool, - ) -> impl Future { - use postage::prelude::{Sink as _, Stream as _}; - - let (tx, mut rx) = postage::mpsc::channel(1024); - let timeout_duration = cx.condition_duration(); - - let mut cx = cx.cx.borrow_mut(); - let subscriptions = self.update(&mut *cx, |_, cx| { - ( - cx.observe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }), - cx.subscribe(self, { - let mut tx = tx.clone(); - move |_, _, _, _| { - tx.blocking_send(()).ok(); - } - }), - ) - }); - - let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); - let handle = self.downgrade(); - - async move { - crate::util::timeout(timeout_duration, async move { - loop { - { - let cx = cx.borrow(); - let cx = cx.as_ref(); - if predicate( - handle - .upgrade(cx) - .expect("view dropped with pending condition") - .read(cx), - cx, - ) { - break; - } - } - - cx.borrow().foreground().start_waiting(); - rx.recv() - .await - .expect("view dropped with pending condition"); - cx.borrow().foreground().finish_waiting(); - } - }) - .await - .expect("condition timed out"); - drop(subscriptions); - } - } } impl Clone for ViewHandle { diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs new file mode 100644 index 0000000000..477c316f71 --- /dev/null +++ b/crates/gpui/src/app/test_app_context.rs @@ -0,0 +1,655 @@ +use std::{ + cell::RefCell, + marker::PhantomData, + mem, + path::PathBuf, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use futures::Future; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock}; +use smol::stream::StreamExt; + +use crate::{ + executor, keymap::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity, + Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, ModelContext, ModelHandle, + MutableAppContext, Platform, ReadModelWith, ReadViewWith, RenderContext, Task, UpdateModel, + UpdateView, View, ViewContext, ViewHandle, WeakHandle, WindowInputHandler, +}; +use collections::BTreeMap; + +use super::{AsyncAppContext, RefCounts}; + +pub struct TestAppContext { + cx: Rc>, + foreground_platform: Rc, + condition_duration: Option, + pub function_name: String, + assertion_context: AssertionContextManager, +} + +impl TestAppContext { + pub fn new( + foreground_platform: Rc, + platform: Arc, + foreground: Rc, + background: Arc, + font_cache: Arc, + leak_detector: Arc>, + first_entity_id: usize, + function_name: String, + ) -> Self { + let mut cx = MutableAppContext::new( + foreground, + background, + platform, + foreground_platform.clone(), + font_cache, + RefCounts { + #[cfg(any(test, feature = "test-support"))] + leak_detector, + ..Default::default() + }, + (), + ); + cx.next_entity_id = first_entity_id; + let cx = TestAppContext { + cx: Rc::new(RefCell::new(cx)), + foreground_platform, + condition_duration: None, + function_name, + assertion_context: AssertionContextManager::new(), + }; + cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx)); + cx + } + + pub fn dispatch_action(&self, window_id: usize, action: A) { + let mut cx = self.cx.borrow_mut(); + if let Some(view_id) = cx.focused_view_id(window_id) { + cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action); + } + } + + pub fn dispatch_global_action(&self, action: A) { + self.cx.borrow_mut().dispatch_global_action(action); + } + + pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { + let handled = self.cx.borrow_mut().update(|cx| { + let presenter = cx + .presenters_and_platform_windows + .get(&window_id) + .unwrap() + .0 + .clone(); + + if cx.dispatch_keystroke(window_id, &keystroke) { + return true; + } + + if presenter.borrow_mut().dispatch_event( + Event::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held, + }), + false, + cx, + ) { + return true; + } + + false + }); + + if !handled && !keystroke.cmd && !keystroke.ctrl { + WindowInputHandler { + app: self.cx.clone(), + window_id, + } + .replace_text_in_range(None, &keystroke.key) + } + } + + pub fn add_model(&mut self, build_model: F) -> ModelHandle + where + T: Entity, + F: FnOnce(&mut ModelContext) -> T, + { + self.cx.borrow_mut().add_model(build_model) + } + + pub fn add_window(&mut self, build_root_view: F) -> (usize, ViewHandle) + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + let (window_id, view) = self + .cx + .borrow_mut() + .add_window(Default::default(), build_root_view); + self.simulate_window_activation(Some(window_id)); + (window_id, view) + } + + pub fn add_view( + &mut self, + parent_handle: impl Into, + build_view: F, + ) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + self.cx.borrow_mut().add_view(parent_handle, build_view) + } + + pub fn window_ids(&self) -> Vec { + self.cx.borrow().window_ids().collect() + } + + pub fn root_view(&self, window_id: usize) -> Option> { + self.cx.borrow().root_view(window_id) + } + + pub fn read T>(&self, callback: F) -> T { + callback(self.cx.borrow().as_ref()) + } + + pub fn update T>(&mut self, callback: F) -> T { + let mut state = self.cx.borrow_mut(); + // Don't increment pending flushes in order for effects to be flushed before the callback + // completes, which is helpful in tests. + let result = callback(&mut *state); + // Flush effects after the callback just in case there are any. This can happen in edge + // cases such as the closure dropping handles. + state.flush_effects(); + result + } + + pub fn render(&mut self, handle: &ViewHandle, f: F) -> T + where + F: FnOnce(&mut V, &mut RenderContext) -> T, + V: View, + { + handle.update(&mut *self.cx.borrow_mut(), |view, cx| { + let mut render_cx = RenderContext { + app: cx, + window_id: handle.window_id(), + view_id: handle.id(), + view_type: PhantomData, + titlebar_height: 0., + hovered_region_ids: Default::default(), + clicked_region_ids: None, + refreshing: false, + appearance: Appearance::Light, + }; + f(view, &mut render_cx) + }) + } + + pub fn to_async(&self) -> AsyncAppContext { + AsyncAppContext(self.cx.clone()) + } + + pub fn font_cache(&self) -> Arc { + self.cx.borrow().cx.font_cache.clone() + } + + pub fn foreground_platform(&self) -> Rc { + self.foreground_platform.clone() + } + + pub fn platform(&self) -> Arc { + self.cx.borrow().cx.platform.clone() + } + + pub fn foreground(&self) -> Rc { + self.cx.borrow().foreground().clone() + } + + pub fn background(&self) -> Arc { + self.cx.borrow().background().clone() + } + + pub fn spawn(&self, f: F) -> Task + where + F: FnOnce(AsyncAppContext) -> Fut, + Fut: 'static + Future, + T: 'static, + { + let foreground = self.foreground(); + let future = f(self.to_async()); + let cx = self.to_async(); + foreground.spawn(async move { + let result = future.await; + cx.0.borrow_mut().flush_effects(); + result + }) + } + + pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option) { + self.foreground_platform.simulate_new_path_selection(result); + } + + pub fn did_prompt_for_new_path(&self) -> bool { + self.foreground_platform.as_ref().did_prompt_for_new_path() + } + + pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) { + use postage::prelude::Sink as _; + + let mut done_tx = self + .window_mut(window_id) + .pending_prompts + .borrow_mut() + .pop_front() + .expect("prompt was not called"); + let _ = done_tx.try_send(answer); + } + + pub fn has_pending_prompt(&self, window_id: usize) -> bool { + let window = self.window_mut(window_id); + let prompts = window.pending_prompts.borrow_mut(); + !prompts.is_empty() + } + + pub fn current_window_title(&self, window_id: usize) -> Option { + self.window_mut(window_id).title.clone() + } + + pub fn simulate_window_close(&self, window_id: usize) -> bool { + let handler = self.window_mut(window_id).should_close_handler.take(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.window_mut(window_id).should_close_handler = Some(handler); + should_close + } else { + false + } + } + + pub fn simulate_window_activation(&self, to_activate: Option) { + let mut handlers = BTreeMap::new(); + { + let mut cx = self.cx.borrow_mut(); + for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows { + let window = window + .as_any_mut() + .downcast_mut::() + .unwrap(); + handlers.insert( + *window_id, + mem::take(&mut window.active_status_change_handlers), + ); + } + }; + let mut handlers = handlers.into_iter().collect::>(); + handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate); + + for (window_id, mut window_handlers) in handlers { + for window_handler in &mut window_handlers { + window_handler(Some(window_id) == to_activate); + } + + self.window_mut(window_id) + .active_status_change_handlers + .extend(window_handlers); + } + } + + pub fn is_window_edited(&self, window_id: usize) -> bool { + self.window_mut(window_id).edited + } + + pub fn leak_detector(&self) -> Arc> { + self.cx.borrow().leak_detector() + } + + pub fn assert_dropped(&self, handle: impl WeakHandle) { + self.cx + .borrow() + .leak_detector() + .lock() + .assert_dropped(handle.id()) + } + + fn window_mut(&self, window_id: usize) -> std::cell::RefMut { + std::cell::RefMut::map(self.cx.borrow_mut(), |state| { + let (_, window) = state + .presenters_and_platform_windows + .get_mut(&window_id) + .unwrap(); + let test_window = window + .as_any_mut() + .downcast_mut::() + .unwrap(); + test_window + }) + } + + pub fn set_condition_duration(&mut self, duration: Option) { + self.condition_duration = duration; + } + + pub fn condition_duration(&self) -> Duration { + self.condition_duration.unwrap_or_else(|| { + if std::env::var("CI").is_ok() { + Duration::from_secs(2) + } else { + Duration::from_millis(500) + } + }) + } + + pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { + self.update(|cx| { + let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); + let expected_content = expected_content.map(|content| content.to_owned()); + assert_eq!(actual_content, expected_content); + }) + } + + pub fn add_assertion_context(&self, context: String) -> ContextHandle { + self.assertion_context.add_context(context) + } + + pub fn assertion_context(&self) -> String { + self.assertion_context.context() + } +} + +impl UpdateModel for TestAppContext { + fn update_model( + &mut self, + handle: &ModelHandle, + update: &mut dyn FnMut(&mut T, &mut ModelContext) -> O, + ) -> O { + self.cx.borrow_mut().update_model(handle, update) + } +} + +impl ReadModelWith for TestAppContext { + fn read_model_with( + &self, + handle: &ModelHandle, + read: &mut dyn FnMut(&E, &AppContext) -> T, + ) -> T { + let cx = self.cx.borrow(); + let cx = cx.as_ref(); + read(handle.read(cx), cx) + } +} + +impl UpdateView for TestAppContext { + fn update_view( + &mut self, + handle: &ViewHandle, + update: &mut dyn FnMut(&mut T, &mut ViewContext) -> S, + ) -> S + where + T: View, + { + self.cx.borrow_mut().update_view(handle, update) + } +} + +impl ReadViewWith for TestAppContext { + fn read_view_with( + &self, + handle: &ViewHandle, + read: &mut dyn FnMut(&V, &AppContext) -> T, + ) -> T + where + V: View, + { + let cx = self.cx.borrow(); + let cx = cx.as_ref(); + read(handle.read(cx), cx) + } +} + +impl ModelHandle { + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + let (tx, mut rx) = futures::channel::mpsc::unbounded(); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.unbounded_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = crate::util::timeout(duration, rx.next()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } + + pub fn next_event(&self, cx: &TestAppContext) -> impl Future + where + T::Event: Clone, + { + let (tx, mut rx) = futures::channel::mpsc::unbounded(); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.subscribe(self, move |_, event, _| { + tx.unbounded_send(event.clone()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + cx.foreground.start_waiting(); + async move { + let event = crate::util::timeout(duration, rx.next()) + .await + .expect("next event timed out"); + drop(subscription); + event.expect("model dropped while test was waiting for its next event") + } + } + + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&T, &AppContext) -> bool, + ) -> impl Future { + let (tx, mut rx) = futures::channel::mpsc::unbounded(); + + let mut cx = cx.cx.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let tx = tx.clone(); + move |_, _| { + tx.unbounded_send(()).ok(); + } + }), + cx.subscribe(self, { + move |_, _, _| { + tx.unbounded_send(()).ok(); + } + }), + ); + + let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); + let handle = self.downgrade(); + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + crate::util::timeout(duration, async move { + loop { + { + let cx = cx.borrow(); + let cx = cx.as_ref(); + if predicate( + handle + .upgrade(cx) + .expect("model dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + cx.borrow().foreground().start_waiting(); + rx.next() + .await + .expect("model dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + +impl ViewHandle { + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = crate::util::timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } + + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&T, &AppContext) -> bool, + ) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + let timeout_duration = cx.condition_duration(); + + let mut cx = cx.cx.borrow_mut(); + let subscriptions = self.update(&mut *cx, |_, cx| { + ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _, _| { + tx.blocking_send(()).ok(); + } + }), + ) + }); + + let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + crate::util::timeout(timeout_duration, async move { + loop { + { + let cx = cx.borrow(); + let cx = cx.as_ref(); + if predicate( + handle + .upgrade(cx) + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + cx.borrow().foreground().start_waiting(); + rx.recv() + .await + .expect("view dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + +#[derive(Clone)] +pub struct AssertionContextManager { + id: Arc, + contexts: Arc>>, +} + +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); + } +} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 3acd3f3a90..44f2a8cb16 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -34,6 +34,7 @@ workspace = { path = "../workspace" } [dev-dependencies] indoc = "1.0.4" parking_lot = "0.11.1" +lazy_static = "1.4" editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 1b9b299bf3..05cd2af1d9 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { 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 = 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) { 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) { .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) { }); 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) { #[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] diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 924fc9d708..cc62ce8db0 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -79,7 +79,7 @@ mod test { use crate::{ state::Mode, - test_contexts::{NeovimBackedTestContext, VimTestContext}, + test::{NeovimBackedTestContext, VimTestContext}, }; #[gpui::test] diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index b0fd3ea5a1..1465e3e377 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -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) { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index b55545682f..a0f9b4a6da 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -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ˇ diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs new file mode 100644 index 0000000000..63bb7996bf --- /dev/null +++ b/crates/vim/src/test.rs @@ -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::() + .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"); + }) +} diff --git a/crates/vim/src/test_contexts/neovim_backed_binding_test_context.rs b/crates/vim/src/test/neovim_backed_binding_test_context.rs similarity index 92% rename from crates/vim/src/test_contexts/neovim_backed_binding_test_context.rs rename to crates/vim/src/test/neovim_backed_binding_test_context.rs index 3f6b8f99f8..a768aff59d 100644 --- a/crates/vim/src/test_contexts/neovim_backed_binding_test_context.rs +++ b/crates/vim/src/test/neovim_backed_binding_test_context.rs @@ -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 diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs new file mode 100644 index 0000000000..bb8ba26b74 --- /dev/null +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -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>>, + 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( + &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::(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( + &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( + &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( + 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; + } +} diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs new file mode 100644 index 0000000000..ff4e10cfe5 --- /dev/null +++ b/crates/vim/src/test/neovim_connection.rs @@ -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), +} + +pub struct NeovimConnection { + data: VecDeque, + #[cfg(feature = "neovim")] + test_case_id: String, + #[cfg(feature = "neovim")] + nvim: Neovim>, + #[cfg(feature = "neovim")] + _join_handle: JoinHandle>>, + #[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, 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::>(); + + nvim_buffer + .set_lines(0, -1, false, lines) + .await + .expect("Could not set nvim buffer text"); + + self.nvim + .input("") + .await + .expect("Could not send escape to nvim"); + self.nvim + .input("") + .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, _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 { + let cursor_row: u32 = self + .nvim + .command_output("echo line('.')") + .await + .unwrap() + .parse::() + .unwrap() + - 1; // Neovim rows start at 1 + let cursor_col: u32 = self + .nvim + .command_output("echo col('.')") + .await + .unwrap() + .parse::() + .unwrap() + - 1; // Neovim columns start at 1 + + let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await { + self.nvim + .input("") + .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 { + // 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 { + 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 { + 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 { + 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>; + + 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; + + async fn handle_request( + &self, + _event_name: String, + _arguments: Vec, + _neovim: Neovim, + ) -> Result { + unimplemented!(); + } + + async fn handle_notify( + &self, + _event_name: String, + _arguments: Vec, + _neovim: Neovim, + ) { + } +} diff --git a/crates/vim/src/test_contexts/vim_binding_test_context.rs b/crates/vim/src/test/vim_binding_test_context.rs similarity index 100% rename from crates/vim/src/test_contexts/vim_binding_test_context.rs rename to crates/vim/src/test/vim_binding_test_context.rs diff --git a/crates/vim/src/test_contexts/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs similarity index 91% rename from crates/vim/src/test_contexts/vim_test_context.rs rename to crates/vim/src/test/vim_test_context.rs index 711e9d610c..2fb446d127 100644 --- a/crates/vim/src/test_contexts/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -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::().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( @@ -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( diff --git a/crates/vim/src/test_contexts.rs b/crates/vim/src/test_contexts.rs deleted file mode 100644 index 1a65be251b..0000000000 --- a/crates/vim/src/test_contexts.rs +++ /dev/null @@ -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::*; diff --git a/crates/vim/src/test_contexts/neovim_backed_test_context.rs b/crates/vim/src/test_contexts/neovim_backed_test_context.rs deleted file mode 100644 index aa52f0c40b..0000000000 --- a/crates/vim/src/test_contexts/neovim_backed_test_context.rs +++ /dev/null @@ -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>>, - 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( - &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::(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::>(); - - nvim_buffer - .set_lines(0, -1, false, lines) - .await - .expect("Could not set nvim buffer text"); - - self.neovim - .input("") - .await - .expect("Could not send escape to nvim"); - self.neovim - .input("") - .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( - &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( - &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( - 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), -} - -struct NeovimConnection { - data: VecDeque, - #[cfg(feature = "neovim")] - test_case_id: String, - #[cfg(feature = "neovim")] - nvim: Neovim>, - #[cfg(feature = "neovim")] - _join_handle: JoinHandle>>, - #[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 { - let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await { - self.nvim - .input("") - .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::() - .unwrap() - - 1; // Neovim rows start at 1 - let nvim_column: u32 = self - .nvim - .command_output("echo col('.')") - .await - .unwrap() - .parse::() - .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 { - 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 { - 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 { - 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 { - 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>; - - 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; - - async fn handle_request( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) -> Result { - unimplemented!(); - } - - async fn handle_notify( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) { - } -} - -#[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; - } -} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 5c9f23b41f..81bafcf3e2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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::() - .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"); - }) - } -} diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index eb222346ce..63d914d570 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -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, ); diff --git a/crates/vim/test_data/test_b.json b/crates/vim/test_data/test_b.json new file mode 100644 index 0000000000..635edf536b --- /dev/null +++ b/crates/vim/test_data/test_b.json @@ -0,0 +1 @@ +[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_cc.json b/crates/vim/test_data/test_cc.json new file mode 100644 index 0000000000..67492d827e --- /dev/null +++ b/crates/vim/test_data/test_cc.json @@ -0,0 +1 @@ +[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_dd.json b/crates/vim/test_data/test_dd.json new file mode 100644 index 0000000000..fa86b9d3b5 --- /dev/null +++ b/crates/vim/test_data/test_dd.json @@ -0,0 +1 @@ +[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_delete_left.json b/crates/vim/test_data/test_delete_left.json new file mode 100644 index 0000000000..06e24f34f7 --- /dev/null +++ b/crates/vim/test_data/test_delete_left.json @@ -0,0 +1 @@ +[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_delete_to_end_of_line.json b/crates/vim/test_data/test_delete_to_end_of_line.json new file mode 100644 index 0000000000..591dac4200 --- /dev/null +++ b/crates/vim/test_data/test_delete_to_end_of_line.json @@ -0,0 +1 @@ +[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index 43d1e0559a..b13aa23589 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -1 +1 @@ -[{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[2,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[2,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,4]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,0],"end":[2,4]}},{"Mode":{"Visual":{"line":false}}}] \ No newline at end of file +[{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[2,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[2,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[0,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[0,4]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[1,0]}},{"Mode":{"Visual":{"line":false}}}] \ No newline at end of file diff --git a/crates/vim/test_data/test_insert_first_non_whitespace.json b/crates/vim/test_data/test_insert_first_non_whitespace.json new file mode 100644 index 0000000000..b64ea3c808 --- /dev/null +++ b/crates/vim/test_data/test_insert_first_non_whitespace.json @@ -0,0 +1 @@ +[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_jump_to_first_non_whitespace.json b/crates/vim/test_data/test_jump_to_first_non_whitespace.json new file mode 100644 index 0000000000..123b752860 --- /dev/null +++ b/crates/vim/test_data/test_jump_to_first_non_whitespace.json @@ -0,0 +1 @@ +[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_o.json b/crates/vim/test_data/test_o.json new file mode 100644 index 0000000000..08bea7cae8 --- /dev/null +++ b/crates/vim/test_data/test_o.json @@ -0,0 +1 @@ +[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"fn test() {\n println!();\n \n}\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"fn test() {\n\n println!();\n}"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_p.json b/crates/vim/test_data/test_p.json new file mode 100644 index 0000000000..2cf45ea2f7 --- /dev/null +++ b/crates/vim/test_data/test_p.json @@ -0,0 +1 @@ +[{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps overjumps o\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,20],"end":[1,20]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_visual_change.json b/crates/vim/test_data/test_visual_change.json new file mode 100644 index 0000000000..c7f6df4445 --- /dev/null +++ b/crates/vim/test_data/test_visual_change.json @@ -0,0 +1 @@ +[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_visual_line_change.json b/crates/vim/test_data/test_visual_line_change.json new file mode 100644 index 0000000000..8c00d1bb1f --- /dev/null +++ b/crates/vim/test_data/test_visual_line_change.json @@ -0,0 +1 @@ +[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_x.json b/crates/vim/test_data/test_x.json new file mode 100644 index 0000000000..ca85e2842b --- /dev/null +++ b/crates/vim/test_data/test_x.json @@ -0,0 +1 @@ +[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}] \ No newline at end of file