WIP

Keith Simmons created

Change summary

crates/editor/src/editor.rs        | 304 ++++++++++++------------------
crates/editor/src/test.rs          | 312 +++++++++++++++++++++++++++++++
crates/vim/src/vim_test_context.rs | 263 ++------------------------
3 files changed, 457 insertions(+), 422 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -5,8 +5,8 @@ pub mod movement;
 mod multi_buffer;
 pub mod selections_collection;
 
-#[cfg(test)]
-mod test;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
 
 use aho_corasick::AhoCorasick;
 use anyhow::Result;
@@ -6017,7 +6017,9 @@ pub fn styled_runs_for_code_label<'a>(
 
 #[cfg(test)]
 mod tests {
-    use crate::test::{assert_text_with_selections, select_ranges};
+    use crate::test::{
+        assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
+    };
 
     use super::*;
     use gpui::{
@@ -7289,117 +7291,62 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let buffer = MultiBuffer::build_simple(
-            indoc! {"
-                  one two
-                three
-                 four"},
-            cx,
-        );
-        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
-
-        view.update(cx, |view, cx| {
-            // two selections on the same line
-            select_ranges(
-                view,
-                indoc! {"
-                      [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // indent from mid-tabstop to full tabstop
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                        [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // outdent from 1 tabstop to 0 tabstops
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // select across line ending
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    t[hree
-                    ] four"},
-                cx,
-            );
+    async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
 
-            // indent and outdent affect only the preceding line
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                        t[hree
-                    ] four"},
-                cx,
-            );
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                    t[hree
-                    ] four"},
-                cx,
-            );
-
-            // Ensure that indenting/outdenting works when the cursor is at column 0.
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    []three
-                     four"},
-                cx,
-            );
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                        []three
-                     four"},
-                cx,
-            );
+        cx.set_state(indoc! {"
+              [one} [two}
+            three
+             four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+                [one} [two}
+            three
+             four"});
 
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    []    three
-                     four"},
-                cx,
-            );
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                    []three
-                     four"},
-                cx,
-            );
-        });
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            [one} [two}
+            three
+             four"});
+
+        // select across line ending
+        cx.set_state(indoc! {"
+            one two
+            t[hree
+            } four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+                t[hree
+            } four"});
+
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+            t[hree
+            } four"});
+
+        // Ensure that indenting/outdenting works when the cursor is at column 0.
+        cx.set_state(indoc! {"
+            one two
+            |three
+                four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+                |three
+                four"});
+
+        cx.set_state(indoc! {"
+            one two
+            |    three
+             four"});
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+            |three
+             four"});
     }
 
     #[gpui::test]
@@ -7508,73 +7455,74 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_backspace(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            build_editor(MultiBuffer::build_simple("", cx), cx)
-        });
-
-        view.update(cx, |view, cx| {
-            view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // an empty selection - the preceding character is deleted
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                    // one character selected - it is deleted
-                    DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-                    // a line suffix selected - it is deleted
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
-                ])
-            });
-            view.backspace(&Backspace, cx);
-            assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
-
-            view.set_text("    one\n        two\n        three\n   four", cx);
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // cursors at the the end of leading indent - last indent is deleted
-                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
-                    DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
-                    // cursors inside leading indent - overlapping indent deletions are coalesced
-                    DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
-                    DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
-                    // cursor at the beginning of a line - preceding newline is deleted
-                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-                    // selection inside leading indent - only the selected character is deleted
-                    DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
-                ])
-            });
-            view.backspace(&Backspace, cx);
-            assert_eq!(view.text(cx), "one\n    two\n  three  four");
-        });
+    async fn test_backspace(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
+        // Basic backspace
+        cx.set_state(indoc! {"
+            on|e two three
+            fou[r} five six
+            seven {eight nine
+            ]ten"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            o|e two three
+            fou| five six
+            seven |ten"});
+
+        // Test backspace inside and around indents
+        cx.set_state(indoc! {"
+            zero
+                |one
+                    |two
+                | | |  three
+            |  |  four"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            zero
+            |one
+                |two
+            |  three|  four"});
+
+        // Test backspace with line_mode set to true
+        cx.update_editor(|e, _| e.selections.line_mode = true);
+        cx.set_state(indoc! {"
+            The |quick |brown
+            fox jumps over
+            the lazy dog
+            |The qu[ick b}rown"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            |
+            fox jumps over
+            the lazy dog|"});
     }
 
     #[gpui::test]
-    fn test_delete(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let buffer =
-            MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
-        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
-
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // an empty selection - the following character is deleted
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                    // one character selected - it is deleted
-                    DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-                    // a line suffix selected - it is deleted
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
-                ])
-            });
-            view.delete(&Delete, cx);
-        });
-
-        assert_eq!(
-            buffer.read(cx).read(cx).text(),
-            "on two three\nfou five six\nseven ten\n"
-        );
+    async fn test_delete(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
+
+        cx.set_state(indoc! {"
+            on|e two three
+            fou[r} five six
+            seven {eight nine
+            ]ten"});
+        cx.update_editor(|e, cx| e.delete(&Delete, cx));
+        cx.assert_editor_state(indoc! {"
+            on| two three
+            fou| five six
+            seven |ten"});
+
+        // Test backspace with line_mode set to true
+        cx.update_editor(|e, _| e.selections.line_mode = true);
+        cx.set_state(indoc! {"
+            The |quick |brown
+            fox {jum]ps over|
+            the lazy dog
+            |The qu[ick b}rown"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            |
+            the lazy dog|"});
     }
 
     #[gpui::test]
@@ -9795,10 +9743,6 @@ mod tests {
         point..point
     }
 
-    fn build_editor(buffer: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
-        Editor::new(EditorMode::Full, buffer, None, None, None, cx)
-    }
-
     fn assert_selection_ranges(
         marked_text: &str,
         selection_marker_pairs: Vec<(char, char)>,

crates/editor/src/test.rs 🔗

@@ -1,9 +1,20 @@
-use gpui::ViewContext;
-use util::test::{marked_text, marked_text_ranges};
+use std::ops::{Deref, DerefMut, Range};
+
+use indoc::indoc;
+
+use collections::BTreeMap;
+use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle};
+use itertools::{Either, Itertools};
+use language::Selection;
+use settings::Settings;
+use util::{
+    set_eq,
+    test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
+};
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-    DisplayPoint, Editor, MultiBuffer,
+    Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer,
 };
 
 #[cfg(test)]
@@ -56,3 +67,298 @@ pub fn assert_text_with_selections(
     assert_eq!(editor.text(cx), unmarked_text);
     assert_eq!(editor.selections.ranges(cx), text_ranges);
 }
+
+pub(crate) fn build_editor(
+    buffer: ModelHandle<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    Editor::new(EditorMode::Full, buffer, None, None, None, cx)
+}
+
+pub struct EditorTestContext<'a> {
+    pub cx: &'a mut gpui::TestAppContext,
+    pub window_id: usize,
+    pub editor: ViewHandle<Editor>,
+}
+
+impl<'a> EditorTestContext<'a> {
+    pub async 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 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 editor_text(&mut self) -> String {
+        self.editor
+            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
+    }
+
+    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+        let input = if keystroke.modified() {
+            None
+        } else {
+            Some(keystroke.key.clone())
+        };
+        self.cx
+            .dispatch_keystroke(self.window_id, keystroke, input, 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);
+        }
+    }
+
+    // Sets the editor state via a marked string.
+    // `|` characters represent empty selections
+    // `[` to `}` represents a non empty selection with the head at `}`
+    // `{` to `]` represents a non empty selection with the head at `{`
+    pub fn set_state(&mut self, text: &str) {
+        self.editor.update(self.cx, |editor, cx| {
+            let (text_with_ranges, empty_selections) = marked_text(&text);
+            let (unmarked_text, mut selection_ranges) =
+                marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
+            editor.set_text(unmarked_text, cx);
+
+            let mut selections: Vec<Range<usize>> = empty_selections
+                .into_iter()
+                .map(|offset| offset..offset)
+                .collect();
+            selections.extend(selection_ranges.remove(&('{', ']')).unwrap_or_default());
+            selections.extend(selection_ranges.remove(&('[', '}')).unwrap_or_default());
+
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections));
+        })
+    }
+
+    // Asserts the editor state via a marked string.
+    // `|` characters represent empty selections
+    // `[` to `}` represents a non empty selection with the head at `}`
+    // `{` to `]` represents a non empty selection with the head at `{`
+    pub fn assert_editor_state(&mut self, text: &str) {
+        let (text_with_ranges, expected_empty_selections) = marked_text(&text);
+        let (unmarked_text, mut selection_ranges) =
+            marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
+        let editor_text = self.editor_text();
+        assert_eq!(
+            editor_text, unmarked_text,
+            "Unmarked text doesn't match editor text"
+        );
+
+        let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
+        let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
+
+        self.assert_selections(
+            expected_empty_selections,
+            expected_reverse_selections,
+            expected_forward_selections,
+            Some(text.to_string()),
+        )
+    }
+
+    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
+        let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
+            expected_selections.into_iter().partition_map(|selection| {
+                if selection.is_empty() {
+                    Either::Left(selection.head())
+                } else {
+                    Either::Right(selection)
+                }
+            });
+
+        let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
+            expected_non_empty_selections
+                .into_iter()
+                .partition_map(|selection| {
+                    let range = selection.start..selection.end;
+                    if selection.reversed {
+                        Either::Left(range)
+                    } else {
+                        Either::Right(range)
+                    }
+                });
+
+        self.assert_selections(
+            expected_empty_selections,
+            expected_reverse_selections,
+            expected_forward_selections,
+            None,
+        )
+    }
+
+    fn assert_selections(
+        &mut self,
+        expected_empty_selections: Vec<usize>,
+        expected_reverse_selections: Vec<Range<usize>>,
+        expected_forward_selections: Vec<Range<usize>>,
+        asserted_text: Option<String>,
+    ) {
+        let (empty_selections, reverse_selections, forward_selections) =
+            self.editor.read_with(self.cx, |editor, cx| {
+                let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
+                    .selections
+                    .all::<usize>(cx)
+                    .into_iter()
+                    .partition_map(|selection| {
+                        if selection.is_empty() {
+                            Either::Left(selection.head())
+                        } else {
+                            Either::Right(selection)
+                        }
+                    });
+
+                let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
+                    non_empty_selections.into_iter().partition_map(|selection| {
+                        let range = selection.start..selection.end;
+                        if selection.reversed {
+                            Either::Left(range)
+                        } else {
+                            Either::Right(range)
+                        }
+                    });
+                (empty_selections, reverse_selections, forward_selections)
+            });
+
+        let asserted_selections = asserted_text.unwrap_or_else(|| {
+            self.insert_markers(
+                &expected_empty_selections,
+                &expected_reverse_selections,
+                &expected_forward_selections,
+            )
+        });
+        let actual_selections =
+            self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
+
+        let unmarked_text = self.editor_text();
+        let all_eq: Result<(), SetEqError<String>> =
+            set_eq!(expected_empty_selections, empty_selections)
+                .map_err(|err| {
+                    err.map(|missing| {
+                        let mut error_text = unmarked_text.clone();
+                        error_text.insert(missing, '|');
+                        error_text
+                    })
+                })
+                .and_then(|_| {
+                    set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
+                        err.map(|missing| {
+                            let mut error_text = unmarked_text.clone();
+                            error_text.insert(missing.start, '{');
+                            error_text.insert(missing.end, ']');
+                            error_text
+                        })
+                    })
+                })
+                .and_then(|_| {
+                    set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
+                        err.map(|missing| {
+                            let mut error_text = unmarked_text.clone();
+                            error_text.insert(missing.start, '[');
+                            error_text.insert(missing.end, '}');
+                            error_text
+                        })
+                    })
+                });
+
+        match all_eq {
+            Err(SetEqError::LeftMissing(location_text)) => {
+                panic!(
+                    indoc! {"
+                        Editor has extra selection
+                        Extra Selection Location:
+                        {}
+                        Asserted selections:
+                        {}
+                        Actual selections:
+                        {}"},
+                    location_text, asserted_selections, actual_selections,
+                );
+            }
+            Err(SetEqError::RightMissing(location_text)) => {
+                panic!(
+                    indoc! {"
+                        Editor is missing empty selection
+                        Missing Selection Location:
+                        {}
+                        Asserted selections:
+                        {}
+                        Actual selections:
+                        {}"},
+                    location_text, asserted_selections, actual_selections,
+                );
+            }
+            _ => {}
+        }
+    }
+
+    fn insert_markers(
+        &mut self,
+        empty_selections: &Vec<usize>,
+        reverse_selections: &Vec<Range<usize>>,
+        forward_selections: &Vec<Range<usize>>,
+    ) -> String {
+        let mut editor_text_with_selections = self.editor_text();
+        let mut selection_marks = BTreeMap::new();
+        for offset in empty_selections {
+            selection_marks.insert(offset, '|');
+        }
+        for range in reverse_selections {
+            selection_marks.insert(&range.start, '{');
+            selection_marks.insert(&range.end, ']');
+        }
+        for range in forward_selections {
+            selection_marks.insert(&range.start, '[');
+            selection_marks.insert(&range.end, '}');
+        }
+        for (offset, mark) in selection_marks.into_iter().rev() {
+            editor_text_with_selections.insert(*offset, mark);
+        }
+
+        editor_text_with_selections
+    }
+
+    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
+        self.cx.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<'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
+    }
+}

crates/vim/src/vim_test_context.rs 🔗

@@ -1,25 +1,14 @@
-use std::ops::{Deref, DerefMut, Range};
+use std::ops::{Deref, DerefMut};
 
-use collections::BTreeMap;
-use itertools::{Either, Itertools};
-
-use editor::{display_map::ToDisplayPoint, Autoscroll};
-use gpui::{json::json, keymap::Keystroke, ViewHandle};
-use indoc::indoc;
-use language::Selection;
+use editor::test::EditorTestContext;
+use gpui::json::json;
 use project::Project;
-use util::{
-    set_eq,
-    test::{marked_text, marked_text_ranges_by, SetEqError},
-};
 use workspace::{pane, AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
 pub struct VimTestContext<'a> {
-    cx: &'a mut gpui::TestAppContext,
-    window_id: usize,
-    editor: ViewHandle<Editor>,
+    cx: EditorTestContext<'a>,
 }
 
 impl<'a> VimTestContext<'a> {
@@ -70,9 +59,11 @@ impl<'a> VimTestContext<'a> {
         editor.update(cx, |_, cx| cx.focus_self());
 
         Self {
-            cx,
-            window_id,
-            editor,
+            cx: EditorTestContext {
+                cx,
+                window_id,
+                editor,
+            },
         }
     }
 
@@ -101,225 +92,13 @@ impl<'a> VimTestContext<'a> {
             .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
     }
 
-    pub fn editor_text(&mut self) -> String {
-        self.editor
-            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
-    }
-
-    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
-        let keystroke = Keystroke::parse(keystroke_text).unwrap();
-        let input = if keystroke.modified() {
-            None
-        } else {
-            Some(keystroke.key.clone())
-        };
-        self.cx
-            .dispatch_keystroke(self.window_id, keystroke, input, 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 set_state(&mut self, text: &str, mode: Mode) {
-        self.cx
-            .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
-        self.editor.update(self.cx, |editor, cx| {
-            let (unmarked_text, markers) = marked_text(&text);
-            editor.set_text(unmarked_text, cx);
-            let cursor_offset = markers[0];
-            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
-            });
-        })
-    }
-
-    // Asserts the editor state via a marked string.
-    // `|` characters represent empty selections
-    // `[` to `}` represents a non empty selection with the head at `}`
-    // `{` to `]` represents a non empty selection with the head at `{`
-    pub fn assert_editor_state(&mut self, text: &str) {
-        let (text_with_ranges, expected_empty_selections) = marked_text(&text);
-        let (unmarked_text, mut selection_ranges) =
-            marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
-        let editor_text = self.editor_text();
-        assert_eq!(
-            editor_text, unmarked_text,
-            "Unmarked text doesn't match editor text"
-        );
-
-        let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
-        let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
-
-        self.assert_selections(
-            expected_empty_selections,
-            expected_reverse_selections,
-            expected_forward_selections,
-            Some(text.to_string()),
-        )
-    }
-
-    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
-        let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
-            expected_selections.into_iter().partition_map(|selection| {
-                if selection.is_empty() {
-                    Either::Left(selection.head())
-                } else {
-                    Either::Right(selection)
-                }
-            });
-
-        let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
-            expected_non_empty_selections
-                .into_iter()
-                .partition_map(|selection| {
-                    let range = selection.start..selection.end;
-                    if selection.reversed {
-                        Either::Left(range)
-                    } else {
-                        Either::Right(range)
-                    }
-                });
-
-        self.assert_selections(
-            expected_empty_selections,
-            expected_reverse_selections,
-            expected_forward_selections,
-            None,
-        )
-    }
-
-    fn assert_selections(
-        &mut self,
-        expected_empty_selections: Vec<usize>,
-        expected_reverse_selections: Vec<Range<usize>>,
-        expected_forward_selections: Vec<Range<usize>>,
-        asserted_text: Option<String>,
-    ) {
-        let (empty_selections, reverse_selections, forward_selections) =
-            self.editor.read_with(self.cx, |editor, cx| {
-                let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
-                    .selections
-                    .all::<usize>(cx)
-                    .into_iter()
-                    .partition_map(|selection| {
-                        if selection.is_empty() {
-                            Either::Left(selection.head())
-                        } else {
-                            Either::Right(selection)
-                        }
-                    });
-
-                let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
-                    non_empty_selections.into_iter().partition_map(|selection| {
-                        let range = selection.start..selection.end;
-                        if selection.reversed {
-                            Either::Left(range)
-                        } else {
-                            Either::Right(range)
-                        }
-                    });
-                (empty_selections, reverse_selections, forward_selections)
-            });
-
-        let asserted_selections = asserted_text.unwrap_or_else(|| {
-            self.insert_markers(
-                &expected_empty_selections,
-                &expected_reverse_selections,
-                &expected_forward_selections,
-            )
+        self.cx.update(|cx| {
+            Vim::update(cx, |vim, cx| {
+                vim.switch_mode(mode, cx);
+            })
         });
-        let actual_selections =
-            self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
-
-        let unmarked_text = self.editor_text();
-        let all_eq: Result<(), SetEqError<String>> =
-            set_eq!(expected_empty_selections, empty_selections)
-                .map_err(|err| {
-                    err.map(|missing| {
-                        let mut error_text = unmarked_text.clone();
-                        error_text.insert(missing, '|');
-                        error_text
-                    })
-                })
-                .and_then(|_| {
-                    set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
-                        err.map(|missing| {
-                            let mut error_text = unmarked_text.clone();
-                            error_text.insert(missing.start, '{');
-                            error_text.insert(missing.end, ']');
-                            error_text
-                        })
-                    })
-                })
-                .and_then(|_| {
-                    set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
-                        err.map(|missing| {
-                            let mut error_text = unmarked_text.clone();
-                            error_text.insert(missing.start, '[');
-                            error_text.insert(missing.end, '}');
-                            error_text
-                        })
-                    })
-                });
-
-        match all_eq {
-            Err(SetEqError::LeftMissing(location_text)) => {
-                panic!(
-                    indoc! {"
-                        Editor has extra selection
-                        Extra Selection Location:
-                        {}
-                        Asserted selections:
-                        {}
-                        Actual selections:
-                        {}"},
-                    location_text, asserted_selections, actual_selections,
-                );
-            }
-            Err(SetEqError::RightMissing(location_text)) => {
-                panic!(
-                    indoc! {"
-                        Editor is missing empty selection
-                        Missing Selection Location:
-                        {}
-                        Asserted selections:
-                        {}
-                        Actual selections:
-                        {}"},
-                    location_text, asserted_selections, actual_selections,
-                );
-            }
-            _ => {}
-        }
-    }
-
-    fn insert_markers(
-        &mut self,
-        empty_selections: &Vec<usize>,
-        reverse_selections: &Vec<Range<usize>>,
-        forward_selections: &Vec<Range<usize>>,
-    ) -> String {
-        let mut editor_text_with_selections = self.editor_text();
-        let mut selection_marks = BTreeMap::new();
-        for offset in empty_selections {
-            selection_marks.insert(offset, '|');
-        }
-        for range in reverse_selections {
-            selection_marks.insert(&range.start, '{');
-            selection_marks.insert(&range.end, ']');
-        }
-        for range in forward_selections {
-            selection_marks.insert(&range.start, '[');
-            selection_marks.insert(&range.end, '}');
-        }
-        for (offset, mark) in selection_marks.into_iter().rev() {
-            editor_text_with_selections.insert(*offset, mark);
-        }
-
-        editor_text_with_selections
+        self.cx.set_state(text);
     }
 
     pub fn assert_binding<const COUNT: usize>(
@@ -331,8 +110,8 @@ impl<'a> VimTestContext<'a> {
         mode_after: Mode,
     ) {
         self.set_state(initial_state, initial_mode);
-        self.simulate_keystrokes(keystrokes);
-        self.assert_editor_state(state_after);
+        self.cx.simulate_keystrokes(keystrokes);
+        self.cx.assert_editor_state(state_after);
         assert_eq!(self.mode(), mode_after);
         assert_eq!(self.active_operator(), None);
     }
@@ -355,10 +134,16 @@ impl<'a> VimTestContext<'a> {
 }
 
 impl<'a> Deref for VimTestContext<'a> {
-    type Target = gpui::TestAppContext;
+    type Target = EditorTestContext<'a>;
 
     fn deref(&self) -> &Self::Target {
-        self.cx
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for VimTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
     }
 }