1use crate::{
2 display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
3};
4use collections::BTreeMap;
5use futures::Future;
6use gpui::{
7 AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
8};
9use indoc::indoc;
10use itertools::Itertools;
11use language::{Buffer, BufferSnapshot};
12use parking_lot::RwLock;
13use project::{FakeFs, Project};
14use std::{
15 any::TypeId,
16 ops::{Deref, DerefMut, Range},
17 sync::{
18 atomic::{AtomicUsize, Ordering},
19 Arc,
20 },
21};
22use util::{
23 assert_set_eq,
24 test::{generate_marked_text, marked_text_ranges},
25};
26
27use super::build_editor_with_project;
28
29pub struct EditorTestContext {
30 pub cx: gpui::VisualTestContext,
31 pub window: AnyWindowHandle,
32 pub editor: View<Editor>,
33 pub assertion_cx: AssertionContextManager,
34}
35
36impl EditorTestContext {
37 pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
38 let fs = FakeFs::new(cx.executor());
39 // fs.insert_file("/file", "".to_owned()).await;
40 fs.insert_tree(
41 "/root",
42 serde_json::json!({
43 "file": "",
44 }),
45 )
46 .await;
47 let project = Project::test(fs, ["/root".as_ref()], cx).await;
48 let buffer = project
49 .update(cx, |project, cx| {
50 project.open_local_buffer("/root/file", cx)
51 })
52 .await
53 .unwrap();
54 let editor = cx.add_window(|cx| {
55 let editor =
56 build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
57 editor.focus(cx);
58 editor
59 });
60 let editor_view = editor.root_view(cx).unwrap();
61 Self {
62 cx: VisualTestContext::from_window(*editor.deref(), cx),
63 window: editor.into(),
64 editor: editor_view,
65 assertion_cx: AssertionContextManager::new(),
66 }
67 }
68
69 pub fn condition(
70 &self,
71 predicate: impl FnMut(&Editor, &AppContext) -> bool,
72 ) -> impl Future<Output = ()> {
73 self.editor
74 .condition::<crate::EditorEvent>(&self.cx, predicate)
75 }
76
77 #[track_caller]
78 pub fn editor<F, T>(&mut self, read: F) -> T
79 where
80 F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
81 {
82 self.editor
83 .update(&mut self.cx, |this, cx| read(&this, &cx))
84 }
85
86 #[track_caller]
87 pub fn update_editor<F, T>(&mut self, update: F) -> T
88 where
89 F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
90 {
91 self.editor.update(&mut self.cx, update)
92 }
93
94 pub fn multibuffer<F, T>(&mut self, read: F) -> T
95 where
96 F: FnOnce(&MultiBuffer, &AppContext) -> T,
97 {
98 self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
99 }
100
101 pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
102 where
103 F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
104 {
105 self.update_editor(|editor, cx| editor.buffer().update(cx, update))
106 }
107
108 pub fn buffer_text(&mut self) -> String {
109 self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
110 }
111
112 pub fn buffer<F, T>(&mut self, read: F) -> T
113 where
114 F: FnOnce(&Buffer, &AppContext) -> T,
115 {
116 self.multibuffer(|multibuffer, cx| {
117 let buffer = multibuffer.as_singleton().unwrap().read(cx);
118 read(buffer, cx)
119 })
120 }
121
122 pub fn update_buffer<F, T>(&mut self, update: F) -> T
123 where
124 F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
125 {
126 self.update_multibuffer(|multibuffer, cx| {
127 let buffer = multibuffer.as_singleton().unwrap();
128 buffer.update(cx, update)
129 })
130 }
131
132 pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
133 self.buffer(|buffer, _| buffer.snapshot())
134 }
135
136 pub fn add_assertion_context(&self, context: String) -> ContextHandle {
137 self.assertion_cx.add_context(context)
138 }
139
140 pub fn assertion_context(&self) -> String {
141 self.assertion_cx.context()
142 }
143
144 pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
145 let keystroke_under_test_handle =
146 self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
147 let keystroke = Keystroke::parse(keystroke_text).unwrap();
148
149 self.cx.dispatch_keystroke(self.window, keystroke, false);
150
151 keystroke_under_test_handle
152 }
153
154 pub fn simulate_keystrokes<const COUNT: usize>(
155 &mut self,
156 keystroke_texts: [&str; COUNT],
157 ) -> ContextHandle {
158 let keystrokes_under_test_handle =
159 self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
160 for keystroke_text in keystroke_texts.into_iter() {
161 self.simulate_keystroke(keystroke_text);
162 }
163 // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
164 // before returning.
165 // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
166 // quickly races with async actions.
167 self.cx.background_executor.run_until_parked();
168
169 keystrokes_under_test_handle
170 }
171
172 pub fn run_until_parked(&mut self) {
173 self.cx.background_executor.run_until_parked();
174 }
175
176 pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
177 let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
178 assert_eq!(self.buffer_text(), unmarked_text);
179 ranges
180 }
181
182 pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
183 let ranges = self.ranges(marked_text);
184 let snapshot = self
185 .editor
186 .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
187 ranges[0].start.to_display_point(&snapshot)
188 }
189
190 // Returns anchors for the current buffer using `«` and `»`
191 pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
192 let ranges = self.ranges(marked_text);
193 let snapshot = self.buffer_snapshot();
194 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
195 }
196
197 pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
198 let diff_base = diff_base.map(String::from);
199 self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
200 }
201
202 /// Change the editor's text and selections using a string containing
203 /// embedded range markers that represent the ranges and directions of
204 /// each selection.
205 ///
206 /// Returns a context handle so that assertion failures can print what
207 /// editor state was needed to cause the failure.
208 ///
209 /// See the `util::test::marked_text_ranges` function for more information.
210 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
211 let state_context = self.add_assertion_context(format!(
212 "Initial Editor State: \"{}\"",
213 marked_text.escape_debug().to_string()
214 ));
215 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
216 self.editor.update(&mut self.cx, |editor, cx| {
217 editor.set_text(unmarked_text, cx);
218 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
219 s.select_ranges(selection_ranges)
220 })
221 });
222 state_context
223 }
224
225 /// Only change the editor's selections
226 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
227 let state_context = self.add_assertion_context(format!(
228 "Initial Editor State: \"{}\"",
229 marked_text.escape_debug().to_string()
230 ));
231 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
232 self.editor.update(&mut self.cx, |editor, cx| {
233 assert_eq!(editor.text(cx), unmarked_text);
234 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
235 s.select_ranges(selection_ranges)
236 })
237 });
238 state_context
239 }
240
241 /// Make an assertion about the editor's text and the ranges and directions
242 /// of its selections using a string containing embedded range markers.
243 ///
244 /// See the `util::test::marked_text_ranges` function for more information.
245 #[track_caller]
246 pub fn assert_editor_state(&mut self, marked_text: &str) {
247 let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
248 let buffer_text = self.buffer_text();
249
250 if buffer_text != unmarked_text {
251 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}");
252 }
253
254 self.assert_selections(expected_selections, marked_text.to_string())
255 }
256
257 pub fn editor_state(&mut self) -> String {
258 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
259 }
260
261 #[track_caller]
262 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
263 let expected_ranges = self.ranges(marked_text);
264 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
265 let snapshot = editor.snapshot(cx);
266 editor
267 .background_highlights
268 .get(&TypeId::of::<Tag>())
269 .map(|h| h.1.clone())
270 .unwrap_or_default()
271 .into_iter()
272 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
273 .collect()
274 });
275 assert_set_eq!(actual_ranges, expected_ranges);
276 }
277
278 #[track_caller]
279 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
280 let expected_ranges = self.ranges(marked_text);
281 let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
282 let actual_ranges: Vec<Range<usize>> = snapshot
283 .text_highlight_ranges::<Tag>()
284 .map(|ranges| ranges.as_ref().clone().1)
285 .unwrap_or_default()
286 .into_iter()
287 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
288 .collect();
289 assert_set_eq!(actual_ranges, expected_ranges);
290 }
291
292 #[track_caller]
293 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
294 let expected_marked_text =
295 generate_marked_text(&self.buffer_text(), &expected_selections, true);
296 self.assert_selections(expected_selections, expected_marked_text)
297 }
298
299 #[track_caller]
300 fn editor_selections(&mut self) -> Vec<Range<usize>> {
301 self.editor
302 .update(&mut self.cx, |editor, cx| {
303 editor.selections.all::<usize>(cx)
304 })
305 .into_iter()
306 .map(|s| {
307 if s.reversed {
308 s.end..s.start
309 } else {
310 s.start..s.end
311 }
312 })
313 .collect::<Vec<_>>()
314 }
315
316 #[track_caller]
317 fn assert_selections(
318 &mut self,
319 expected_selections: Vec<Range<usize>>,
320 expected_marked_text: String,
321 ) {
322 let actual_selections = self.editor_selections();
323 let actual_marked_text =
324 generate_marked_text(&self.buffer_text(), &actual_selections, true);
325 if expected_selections != actual_selections {
326 panic!(
327 indoc! {"
328
329 {}Editor has unexpected selections.
330
331 Expected selections:
332 {}
333
334 Actual selections:
335 {}
336 "},
337 self.assertion_context(),
338 expected_marked_text,
339 actual_marked_text,
340 );
341 }
342 }
343}
344
345impl Deref for EditorTestContext {
346 type Target = gpui::TestAppContext;
347
348 fn deref(&self) -> &Self::Target {
349 &self.cx
350 }
351}
352
353impl DerefMut for EditorTestContext {
354 fn deref_mut(&mut self) -> &mut Self::Target {
355 &mut self.cx
356 }
357}
358
359/// Tracks string context to be printed when assertions fail.
360/// Often this is done by storing a context string in the manager and returning the handle.
361#[derive(Clone)]
362pub struct AssertionContextManager {
363 id: Arc<AtomicUsize>,
364 contexts: Arc<RwLock<BTreeMap<usize, String>>>,
365}
366
367impl AssertionContextManager {
368 pub fn new() -> Self {
369 Self {
370 id: Arc::new(AtomicUsize::new(0)),
371 contexts: Arc::new(RwLock::new(BTreeMap::new())),
372 }
373 }
374
375 pub fn add_context(&self, context: String) -> ContextHandle {
376 let id = self.id.fetch_add(1, Ordering::Relaxed);
377 let mut contexts = self.contexts.write();
378 contexts.insert(id, context);
379 ContextHandle {
380 id,
381 manager: self.clone(),
382 }
383 }
384
385 pub fn context(&self) -> String {
386 let contexts = self.contexts.read();
387 format!("\n{}\n", contexts.values().join("\n"))
388 }
389}
390
391/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
392/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
393/// the state that was set initially for the failure can be printed in the error message
394pub struct ContextHandle {
395 id: usize,
396 manager: AssertionContextManager,
397}
398
399impl Drop for ContextHandle {
400 fn drop(&mut self) {
401 let mut contexts = self.manager.contexts.write();
402 contexts.remove(&self.id);
403 }
404}