1use std::ops::Deref;
2
3use editor::{display_map::ToDisplayPoint, Bias, DisplayPoint};
4use gpui::{json::json, keymap::Keystroke, ViewHandle};
5use language::{Point, Selection};
6use util::test::marked_text;
7use workspace::{WorkspaceHandle, WorkspaceParams};
8
9use crate::{state::Operator, *};
10
11pub struct VimTestContext<'a> {
12 cx: &'a mut gpui::TestAppContext,
13 window_id: usize,
14 editor: ViewHandle<Editor>,
15}
16
17impl<'a> VimTestContext<'a> {
18 pub async fn new(
19 cx: &'a mut gpui::TestAppContext,
20 enabled: bool,
21 initial_editor_text: &str,
22 ) -> VimTestContext<'a> {
23 cx.update(|cx| {
24 editor::init(cx);
25 crate::init(cx);
26
27 settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
28 });
29
30 let params = cx.update(WorkspaceParams::test);
31
32 cx.update(|cx| {
33 cx.update_global(|settings: &mut Settings, _| {
34 settings.vim_mode = enabled;
35 });
36 });
37
38 params
39 .fs
40 .as_fake()
41 .insert_tree(
42 "/root",
43 json!({ "dir": { "test.txt": initial_editor_text } }),
44 )
45 .await;
46
47 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
48 params
49 .project
50 .update(cx, |project, cx| {
51 project.find_or_create_local_worktree("/root", true, cx)
52 })
53 .await
54 .unwrap();
55 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
56 .await;
57
58 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
59 let item = workspace
60 .update(cx, |workspace, cx| workspace.open_path(file, cx))
61 .await
62 .expect("Could not open test file");
63
64 let editor = cx.update(|cx| {
65 item.act_as::<Editor>(cx)
66 .expect("Opened test file wasn't an editor")
67 });
68 editor.update(cx, |_, cx| cx.focus_self());
69
70 Self {
71 cx,
72 window_id,
73 editor,
74 }
75 }
76
77 pub fn enable_vim(&mut self) {
78 self.cx.update(|cx| {
79 cx.update_global(|settings: &mut Settings, _| {
80 settings.vim_mode = true;
81 });
82 })
83 }
84
85 pub fn disable_vim(&mut self) {
86 self.cx.update(|cx| {
87 cx.update_global(|settings: &mut Settings, _| {
88 settings.vim_mode = false;
89 });
90 })
91 }
92
93 pub fn newest_selection(&mut self) -> Selection<DisplayPoint> {
94 self.editor.update(self.cx, |editor, cx| {
95 let snapshot = editor.snapshot(cx);
96 editor
97 .newest_selection::<Point>(cx)
98 .map(|point| point.to_display_point(&snapshot.display_snapshot))
99 })
100 }
101
102 pub fn mode(&mut self) -> Mode {
103 self.cx.read(|cx| cx.global::<Vim>().state.mode)
104 }
105
106 pub fn active_operator(&mut self) -> Option<Operator> {
107 self.cx
108 .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
109 }
110
111 pub fn editor_text(&mut self) -> String {
112 self.editor
113 .update(self.cx, |editor, cx| editor.snapshot(cx).text())
114 }
115
116 pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
117 let keystroke = Keystroke::parse(keystroke_text).unwrap();
118 let input = if keystroke.modified() {
119 None
120 } else {
121 Some(keystroke.key.clone())
122 };
123 self.cx
124 .dispatch_keystroke(self.window_id, keystroke, input, false);
125 }
126
127 pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
128 for keystroke_text in keystroke_texts.into_iter() {
129 self.simulate_keystroke(keystroke_text);
130 }
131 }
132
133 pub fn set_state(&mut self, text: &str, mode: Mode) {
134 self.cx
135 .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
136 self.editor.update(self.cx, |editor, cx| {
137 let (unmarked_text, markers) = marked_text(&text);
138 editor.set_text(unmarked_text, cx);
139 let cursor_offset = markers[0];
140 editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
141 })
142 }
143
144 pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
145 let actual_head = self.newest_selection().head();
146 let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
147 let snapshot = editor.snapshot(cx);
148 (
149 actual_head.to_offset(&snapshot, Bias::Left),
150 expected_offset.to_display_point(&snapshot),
151 )
152 });
153 let mut actual_position_text = self.editor_text();
154 let mut expected_position_text = actual_position_text.clone();
155 actual_position_text.insert(actual_offset, '|');
156 expected_position_text.insert(expected_offset, '|');
157 assert_eq!(
158 actual_head, expected_head,
159 "\nActual Position: {}\nExpected Position: {}",
160 actual_position_text, expected_position_text
161 )
162 }
163
164 pub fn assert_editor_state(&mut self, text: &str) {
165 let (unmarked_text, markers) = marked_text(&text);
166 let editor_text = self.editor_text();
167 assert_eq!(
168 editor_text, unmarked_text,
169 "Unmarked text doesn't match editor text"
170 );
171 let expected_offset = markers[0];
172 let actual_head = self.newest_selection().head();
173 let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
174 let snapshot = editor.snapshot(cx);
175 (
176 actual_head.to_offset(&snapshot, Bias::Left),
177 expected_offset.to_display_point(&snapshot),
178 )
179 });
180 let mut actual_position_text = self.editor_text();
181 let mut expected_position_text = actual_position_text.clone();
182 actual_position_text.insert(actual_offset, '|');
183 expected_position_text.insert(expected_offset, '|');
184 assert_eq!(
185 actual_head, expected_head,
186 "\nActual Position: {}\nExpected Position: {}",
187 actual_position_text, expected_position_text
188 )
189 }
190
191 pub fn assert_binding<const COUNT: usize>(
192 &mut self,
193 keystrokes: [&str; COUNT],
194 initial_state: &str,
195 initial_mode: Mode,
196 state_after: &str,
197 mode_after: Mode,
198 ) {
199 self.set_state(initial_state, initial_mode);
200 self.simulate_keystrokes(keystrokes);
201 self.assert_editor_state(state_after);
202 assert_eq!(self.mode(), mode_after);
203 assert_eq!(self.active_operator(), None);
204 }
205}
206
207impl<'a> Deref for VimTestContext<'a> {
208 type Target = gpui::TestAppContext;
209
210 fn deref(&self) -> &Self::Target {
211 self.cx
212 }
213}