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