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