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