1use crate::{
2 display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
3};
4use futures::Future;
5use gpui::{
6 executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
7 ModelContext, ViewContext, ViewHandle,
8};
9use indoc::indoc;
10use language::{Buffer, BufferSnapshot};
11use project::{FakeFs, Project};
12use std::{
13 any::TypeId,
14 ops::{Deref, DerefMut, Range},
15};
16use util::{
17 assert_set_eq,
18 test::{generate_marked_text, marked_text_ranges},
19};
20
21use super::build_editor;
22
23pub struct EditorTestContext<'a> {
24 pub cx: &'a mut gpui::TestAppContext,
25 pub window: AnyWindowHandle,
26 pub editor: ViewHandle<Editor>,
27}
28
29impl<'a> EditorTestContext<'a> {
30 pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
31 let fs = FakeFs::new(cx.background());
32 let project = Project::test(fs, [], cx).await;
33 let buffer = project
34 .update(cx, |project, cx| project.create_buffer("", None, cx))
35 .unwrap();
36 let window = cx.add_window(|cx| {
37 cx.focus_self();
38 build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
39 });
40 let editor = window.root(cx);
41 Self {
42 cx,
43 window: window.into(),
44 editor,
45 }
46 }
47
48 pub fn condition(
49 &self,
50 predicate: impl FnMut(&Editor, &AppContext) -> bool,
51 ) -> impl Future<Output = ()> {
52 self.editor.condition(self.cx, predicate)
53 }
54
55 pub fn editor<F, T>(&self, read: F) -> T
56 where
57 F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
58 {
59 self.editor.read_with(self.cx, read)
60 }
61
62 pub fn update_editor<F, T>(&mut self, update: F) -> T
63 where
64 F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
65 {
66 self.editor.update(self.cx, update)
67 }
68
69 pub fn multibuffer<F, T>(&self, read: F) -> T
70 where
71 F: FnOnce(&MultiBuffer, &AppContext) -> T,
72 {
73 self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
74 }
75
76 pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
77 where
78 F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
79 {
80 self.update_editor(|editor, cx| editor.buffer().update(cx, update))
81 }
82
83 pub fn buffer_text(&self) -> String {
84 self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
85 }
86
87 pub fn buffer<F, T>(&self, read: F) -> T
88 where
89 F: FnOnce(&Buffer, &AppContext) -> T,
90 {
91 self.multibuffer(|multibuffer, cx| {
92 let buffer = multibuffer.as_singleton().unwrap().read(cx);
93 read(buffer, cx)
94 })
95 }
96
97 pub fn update_buffer<F, T>(&mut self, update: F) -> T
98 where
99 F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
100 {
101 self.update_multibuffer(|multibuffer, cx| {
102 let buffer = multibuffer.as_singleton().unwrap();
103 buffer.update(cx, update)
104 })
105 }
106
107 pub fn buffer_snapshot(&self) -> BufferSnapshot {
108 self.buffer(|buffer, _| buffer.snapshot())
109 }
110
111 pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
112 let keystroke_under_test_handle =
113 self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
114 let keystroke = Keystroke::parse(keystroke_text).unwrap();
115
116 self.cx.dispatch_keystroke(self.window, keystroke, false);
117
118 keystroke_under_test_handle
119 }
120
121 pub fn simulate_keystrokes<const COUNT: usize>(
122 &mut self,
123 keystroke_texts: [&str; COUNT],
124 ) -> ContextHandle {
125 let keystrokes_under_test_handle =
126 self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
127 for keystroke_text in keystroke_texts.into_iter() {
128 self.simulate_keystroke(keystroke_text);
129 }
130 // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
131 // before returning.
132 // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
133 // quickly races with async actions.
134 if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
135 executor.run_until_parked();
136 } else {
137 unreachable!();
138 }
139
140 keystrokes_under_test_handle
141 }
142
143 pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
144 let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
145 assert_eq!(self.buffer_text(), unmarked_text);
146 ranges
147 }
148
149 pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
150 let ranges = self.ranges(marked_text);
151 let snapshot = self
152 .editor
153 .update(self.cx, |editor, cx| editor.snapshot(cx));
154 ranges[0].start.to_display_point(&snapshot)
155 }
156
157 // Returns anchors for the current buffer using `«` and `»`
158 pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
159 let ranges = self.ranges(marked_text);
160 let snapshot = self.buffer_snapshot();
161 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
162 }
163
164 pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
165 let diff_base = diff_base.map(String::from);
166 self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
167 }
168
169 /// Change the editor's text and selections using a string containing
170 /// embedded range markers that represent the ranges and directions of
171 /// each selection.
172 ///
173 /// Returns a context handle so that assertion failures can print what
174 /// editor state was needed to cause the failure.
175 ///
176 /// See the `util::test::marked_text_ranges` function for more information.
177 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
178 let state_context = self.add_assertion_context(format!(
179 "Initial Editor State: \"{}\"",
180 marked_text.escape_debug().to_string()
181 ));
182 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
183 self.editor.update(self.cx, |editor, cx| {
184 editor.set_text(unmarked_text, cx);
185 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
186 s.select_ranges(selection_ranges)
187 })
188 });
189 state_context
190 }
191
192 /// Only change the editor's selections
193 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
194 let state_context = self.add_assertion_context(format!(
195 "Initial Editor State: \"{}\"",
196 marked_text.escape_debug().to_string()
197 ));
198 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
199 self.editor.update(self.cx, |editor, cx| {
200 assert_eq!(editor.text(cx), unmarked_text);
201 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
202 s.select_ranges(selection_ranges)
203 })
204 });
205 state_context
206 }
207
208 /// Make an assertion about the editor's text and the ranges and directions
209 /// of its selections using a string containing embedded range markers.
210 ///
211 /// See the `util::test::marked_text_ranges` function for more information.
212 #[track_caller]
213 pub fn assert_editor_state(&mut self, marked_text: &str) {
214 let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
215 let buffer_text = self.buffer_text();
216
217 if buffer_text != unmarked_text {
218 panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
219 }
220
221 self.assert_selections(expected_selections, marked_text.to_string())
222 }
223
224 pub fn editor_state(&mut self) -> String {
225 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
226 }
227
228 #[track_caller]
229 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
230 let expected_ranges = self.ranges(marked_text);
231 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
232 let snapshot = editor.snapshot(cx);
233 editor
234 .background_highlights
235 .get(&TypeId::of::<Tag>())
236 .map(|h| h.1.clone())
237 .unwrap_or_default()
238 .into_iter()
239 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
240 .collect()
241 });
242 assert_set_eq!(actual_ranges, expected_ranges);
243 }
244
245 #[track_caller]
246 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
247 let expected_ranges = self.ranges(marked_text);
248 let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
249 let actual_ranges: Vec<Range<usize>> = snapshot
250 .text_highlight_ranges::<Tag>()
251 .map(|ranges| ranges.as_ref().clone().1)
252 .unwrap_or_default()
253 .into_iter()
254 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
255 .collect();
256 assert_set_eq!(actual_ranges, expected_ranges);
257 }
258
259 #[track_caller]
260 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
261 let expected_marked_text =
262 generate_marked_text(&self.buffer_text(), &expected_selections, true);
263 self.assert_selections(expected_selections, expected_marked_text)
264 }
265
266 fn editor_selections(&self) -> Vec<Range<usize>> {
267 self.editor
268 .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
269 .into_iter()
270 .map(|s| {
271 if s.reversed {
272 s.end..s.start
273 } else {
274 s.start..s.end
275 }
276 })
277 .collect::<Vec<_>>()
278 }
279
280 #[track_caller]
281 fn assert_selections(
282 &mut self,
283 expected_selections: Vec<Range<usize>>,
284 expected_marked_text: String,
285 ) {
286 let actual_selections = self.editor_selections();
287 let actual_marked_text =
288 generate_marked_text(&self.buffer_text(), &actual_selections, true);
289 if expected_selections != actual_selections {
290 panic!(
291 indoc! {"
292
293 {}Editor has unexpected selections.
294
295 Expected selections:
296 {}
297
298 Actual selections:
299 {}
300 "},
301 self.assertion_context(),
302 expected_marked_text,
303 actual_marked_text,
304 );
305 }
306 }
307}
308
309impl<'a> Deref for EditorTestContext<'a> {
310 type Target = gpui::TestAppContext;
311
312 fn deref(&self) -> &Self::Target {
313 self.cx
314 }
315}
316
317impl<'a> DerefMut for EditorTestContext<'a> {
318 fn deref_mut(&mut self) -> &mut Self::Target {
319 &mut self.cx
320 }
321}