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};
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 update_buffer<F, T>(&mut self, update: F) -> T
124 where
125 F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
126 {
127 self.update_multibuffer(|multibuffer, cx| {
128 let buffer = multibuffer.as_singleton().unwrap();
129 buffer.update(cx, update)
130 })
131 }
132
133 pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
134 self.buffer(|buffer, _| buffer.snapshot())
135 }
136
137 pub fn add_assertion_context(&self, context: String) -> ContextHandle {
138 self.assertion_cx.add_context(context)
139 }
140
141 pub fn assertion_context(&self) -> String {
142 self.assertion_cx.context()
143 }
144
145 pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
146 let keystroke_under_test_handle =
147 self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
148 let keystroke = Keystroke::parse(keystroke_text).unwrap();
149
150 self.cx.dispatch_keystroke(self.window, keystroke);
151
152 keystroke_under_test_handle
153 }
154
155 pub fn simulate_keystrokes<const COUNT: usize>(
156 &mut self,
157 keystroke_texts: [&str; COUNT],
158 ) -> ContextHandle {
159 let keystrokes_under_test_handle =
160 self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
161 for keystroke_text in keystroke_texts.into_iter() {
162 self.simulate_keystroke(keystroke_text);
163 }
164 // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
165 // before returning.
166 // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
167 // quickly races with async actions.
168 self.cx.background_executor.run_until_parked();
169
170 keystrokes_under_test_handle
171 }
172
173 pub fn run_until_parked(&mut self) {
174 self.cx.background_executor.run_until_parked();
175 }
176
177 pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
178 let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
179 assert_eq!(self.buffer_text(), unmarked_text);
180 ranges
181 }
182
183 pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
184 let ranges = self.ranges(marked_text);
185 let snapshot = self
186 .editor
187 .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
188 ranges[0].start.to_display_point(&snapshot)
189 }
190
191 pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
192 let display_point = self.display_point(marked_text);
193 self.pixel_position_for(display_point)
194 }
195
196 pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
197 self.update_editor(|editor, cx| {
198 let newest_point = editor.selections.newest_display(cx).head();
199 let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
200 let line_height = editor
201 .style()
202 .unwrap()
203 .text
204 .line_height_in_pixels(cx.rem_size());
205 let snapshot = editor.snapshot(cx);
206 let details = editor.text_layout_details(cx);
207
208 let y = pixel_position.y
209 + line_height * (display_point.row() as f32 - newest_point.row() as f32);
210 let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
211 - snapshot.x_for_display_point(newest_point, &details);
212 Point::new(x, y)
213 })
214 }
215
216 // Returns anchors for the current buffer using `«` and `»`
217 pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
218 let ranges = self.ranges(marked_text);
219 let snapshot = self.buffer_snapshot();
220 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
221 }
222
223 pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
224 let diff_base = diff_base.map(String::from);
225 self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
226 }
227
228 /// Change the editor's text and selections using a string containing
229 /// embedded range markers that represent the ranges and directions of
230 /// each selection.
231 ///
232 /// Returns a context handle so that assertion failures can print what
233 /// editor state was needed to cause the failure.
234 ///
235 /// See the `util::test::marked_text_ranges` function for more information.
236 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
237 let state_context = self.add_assertion_context(format!(
238 "Initial Editor State: \"{}\"",
239 marked_text.escape_debug().to_string()
240 ));
241 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
242 self.editor.update(&mut self.cx, |editor, cx| {
243 editor.set_text(unmarked_text, cx);
244 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
245 s.select_ranges(selection_ranges)
246 })
247 });
248 state_context
249 }
250
251 /// Only change the editor's selections
252 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
253 let state_context = self.add_assertion_context(format!(
254 "Initial Editor State: \"{}\"",
255 marked_text.escape_debug().to_string()
256 ));
257 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
258 self.editor.update(&mut self.cx, |editor, cx| {
259 assert_eq!(editor.text(cx), unmarked_text);
260 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
261 s.select_ranges(selection_ranges)
262 })
263 });
264 state_context
265 }
266
267 /// Make an assertion about the editor's text and the ranges and directions
268 /// of its selections using a string containing embedded range markers.
269 ///
270 /// See the `util::test::marked_text_ranges` function for more information.
271 #[track_caller]
272 pub fn assert_editor_state(&mut self, marked_text: &str) {
273 let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
274 let buffer_text = self.buffer_text();
275
276 if buffer_text != unmarked_text {
277 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}");
278 }
279
280 self.assert_selections(expected_selections, marked_text.to_string())
281 }
282
283 pub fn editor_state(&mut self) -> String {
284 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
285 }
286
287 #[track_caller]
288 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
289 let expected_ranges = self.ranges(marked_text);
290 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
291 let snapshot = editor.snapshot(cx);
292 editor
293 .background_highlights
294 .get(&TypeId::of::<Tag>())
295 .map(|h| h.1.clone())
296 .unwrap_or_default()
297 .into_iter()
298 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
299 .collect()
300 });
301 assert_set_eq!(actual_ranges, expected_ranges);
302 }
303
304 #[track_caller]
305 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
306 let expected_ranges = self.ranges(marked_text);
307 let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
308 let actual_ranges: Vec<Range<usize>> = snapshot
309 .text_highlight_ranges::<Tag>()
310 .map(|ranges| ranges.as_ref().clone().1)
311 .unwrap_or_default()
312 .into_iter()
313 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
314 .collect();
315 assert_set_eq!(actual_ranges, expected_ranges);
316 }
317
318 #[track_caller]
319 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
320 let expected_marked_text =
321 generate_marked_text(&self.buffer_text(), &expected_selections, true);
322 self.assert_selections(expected_selections, expected_marked_text)
323 }
324
325 #[track_caller]
326 fn editor_selections(&mut self) -> Vec<Range<usize>> {
327 self.editor
328 .update(&mut self.cx, |editor, cx| {
329 editor.selections.all::<usize>(cx)
330 })
331 .into_iter()
332 .map(|s| {
333 if s.reversed {
334 s.end..s.start
335 } else {
336 s.start..s.end
337 }
338 })
339 .collect::<Vec<_>>()
340 }
341
342 #[track_caller]
343 fn assert_selections(
344 &mut self,
345 expected_selections: Vec<Range<usize>>,
346 expected_marked_text: String,
347 ) {
348 let actual_selections = self.editor_selections();
349 let actual_marked_text =
350 generate_marked_text(&self.buffer_text(), &actual_selections, true);
351 if expected_selections != actual_selections {
352 panic!(
353 indoc! {"
354
355 {}Editor has unexpected selections.
356
357 Expected selections:
358 {}
359
360 Actual selections:
361 {}
362 "},
363 self.assertion_context(),
364 expected_marked_text,
365 actual_marked_text,
366 );
367 }
368 }
369}
370
371impl Deref for EditorTestContext {
372 type Target = gpui::VisualTestContext;
373
374 fn deref(&self) -> &Self::Target {
375 &self.cx
376 }
377}
378
379impl DerefMut for EditorTestContext {
380 fn deref_mut(&mut self) -> &mut Self::Target {
381 &mut self.cx
382 }
383}
384
385/// Tracks string context to be printed when assertions fail.
386/// Often this is done by storing a context string in the manager and returning the handle.
387#[derive(Clone)]
388pub struct AssertionContextManager {
389 id: Arc<AtomicUsize>,
390 contexts: Arc<RwLock<BTreeMap<usize, String>>>,
391}
392
393impl AssertionContextManager {
394 pub fn new() -> Self {
395 Self {
396 id: Arc::new(AtomicUsize::new(0)),
397 contexts: Arc::new(RwLock::new(BTreeMap::new())),
398 }
399 }
400
401 pub fn add_context(&self, context: String) -> ContextHandle {
402 let id = self.id.fetch_add(1, Ordering::Relaxed);
403 let mut contexts = self.contexts.write();
404 contexts.insert(id, context);
405 ContextHandle {
406 id,
407 manager: self.clone(),
408 }
409 }
410
411 pub fn context(&self) -> String {
412 let contexts = self.contexts.read();
413 format!("\n{}\n", contexts.values().join("\n"))
414 }
415}
416
417/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
418/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
419/// the state that was set initially for the failure can be printed in the error message
420pub struct ContextHandle {
421 id: usize,
422 manager: AssertionContextManager,
423}
424
425impl Drop for ContextHandle {
426 fn drop(&mut self) {
427 let mut contexts = self.manager.contexts.write();
428 contexts.remove(&self.id);
429 }
430}