1use crate::{
2 display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
3 RowExt,
4};
5use collections::BTreeMap;
6use futures::Future;
7use git::diff::DiffHunkStatus;
8use gpui::{
9 prelude::*, AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point,
10 VisualTestContext, Window, WindowHandle,
11};
12use itertools::Itertools;
13use language::{Buffer, BufferSnapshot, LanguageRegistry};
14use multi_buffer::{ExcerptRange, MultiBufferRow};
15use parking_lot::RwLock;
16use project::{FakeFs, Project};
17use std::{
18 any::TypeId,
19 ops::{Deref, DerefMut, Range},
20 path::Path,
21 sync::{
22 atomic::{AtomicUsize, Ordering},
23 Arc,
24 },
25};
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: Entity<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 let root = Self::root_path();
44 fs.insert_tree(
45 root,
46 serde_json::json!({
47 ".git": {},
48 "file": "",
49 }),
50 )
51 .await;
52 let project = Project::test(fs.clone(), [root], cx).await;
53 let buffer = project
54 .update(cx, |project, cx| {
55 project.open_local_buffer(root.join("file"), cx)
56 })
57 .await
58 .unwrap();
59 let editor = cx.add_window(|window, cx| {
60 let editor = build_editor_with_project(
61 project,
62 MultiBuffer::build_from_buffer(buffer, cx),
63 window,
64 cx,
65 );
66
67 window.focus(&editor.focus_handle(cx));
68 editor
69 });
70 let editor_view = editor.root(cx).unwrap();
71
72 cx.run_until_parked();
73 Self {
74 cx: VisualTestContext::from_window(*editor.deref(), cx),
75 window: editor.into(),
76 editor: editor_view,
77 assertion_cx: AssertionContextManager::new(),
78 }
79 }
80
81 #[cfg(target_os = "windows")]
82 fn root_path() -> &'static Path {
83 Path::new("C:\\root")
84 }
85
86 #[cfg(not(target_os = "windows"))]
87 fn root_path() -> &'static Path {
88 Path::new("/root")
89 }
90
91 pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
92 let editor_view = editor.root(cx).unwrap();
93 Self {
94 cx: VisualTestContext::from_window(*editor.deref(), cx),
95 window: editor.into(),
96 editor: editor_view,
97 assertion_cx: AssertionContextManager::new(),
98 }
99 }
100
101 pub fn new_multibuffer<const COUNT: usize>(
102 cx: &mut gpui::TestAppContext,
103 excerpts: [&str; COUNT],
104 ) -> EditorTestContext {
105 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
106 let buffer = cx.new(|cx| {
107 for excerpt in excerpts.into_iter() {
108 let (text, ranges) = marked_text_ranges(excerpt, false);
109 let buffer = cx.new(|cx| Buffer::local(text, cx));
110 multibuffer.push_excerpts(
111 buffer,
112 ranges.into_iter().map(|range| ExcerptRange {
113 context: range,
114 primary: None,
115 }),
116 cx,
117 );
118 }
119 multibuffer
120 });
121
122 let editor = cx.add_window(|window, cx| {
123 let editor = build_editor(buffer, window, cx);
124 window.focus(&editor.focus_handle(cx));
125
126 editor
127 });
128
129 let editor_view = editor.root(cx).unwrap();
130 Self {
131 cx: VisualTestContext::from_window(*editor.deref(), cx),
132 window: editor.into(),
133 editor: editor_view,
134 assertion_cx: AssertionContextManager::new(),
135 }
136 }
137
138 pub fn condition(
139 &self,
140 predicate: impl FnMut(&Editor, &App) -> bool,
141 ) -> impl Future<Output = ()> {
142 self.editor
143 .condition::<crate::EditorEvent>(&self.cx, predicate)
144 }
145
146 #[track_caller]
147 pub fn editor<F, T>(&mut self, read: F) -> T
148 where
149 F: FnOnce(&Editor, &Window, &mut Context<Editor>) -> T,
150 {
151 self.editor
152 .update_in(&mut self.cx, |this, window, cx| read(this, window, cx))
153 }
154
155 #[track_caller]
156 pub fn update_editor<F, T>(&mut self, update: F) -> T
157 where
158 F: FnOnce(&mut Editor, &mut Window, &mut Context<Editor>) -> T,
159 {
160 self.editor.update_in(&mut self.cx, update)
161 }
162
163 pub fn multibuffer<F, T>(&mut self, read: F) -> T
164 where
165 F: FnOnce(&MultiBuffer, &App) -> T,
166 {
167 self.editor(|editor, _, cx| read(editor.buffer().read(cx), cx))
168 }
169
170 pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
171 where
172 F: FnOnce(&mut MultiBuffer, &mut Context<MultiBuffer>) -> T,
173 {
174 self.update_editor(|editor, _, cx| editor.buffer().update(cx, update))
175 }
176
177 pub fn buffer_text(&mut self) -> String {
178 self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
179 }
180
181 pub fn display_text(&mut self) -> String {
182 self.update_editor(|editor, _, cx| editor.display_text(cx))
183 }
184
185 pub fn buffer<F, T>(&mut self, read: F) -> T
186 where
187 F: FnOnce(&Buffer, &App) -> T,
188 {
189 self.multibuffer(|multibuffer, cx| {
190 let buffer = multibuffer.as_singleton().unwrap().read(cx);
191 read(buffer, cx)
192 })
193 }
194
195 pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
196 self.editor(|editor, _, cx| {
197 editor
198 .project
199 .as_ref()
200 .unwrap()
201 .read(cx)
202 .languages()
203 .clone()
204 })
205 }
206
207 pub fn update_buffer<F, T>(&mut self, update: F) -> T
208 where
209 F: FnOnce(&mut Buffer, &mut Context<Buffer>) -> T,
210 {
211 self.update_multibuffer(|multibuffer, cx| {
212 let buffer = multibuffer.as_singleton().unwrap();
213 buffer.update(cx, update)
214 })
215 }
216
217 pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
218 self.buffer(|buffer, _| buffer.snapshot())
219 }
220
221 pub fn add_assertion_context(&self, context: String) -> ContextHandle {
222 self.assertion_cx.add_context(context)
223 }
224
225 pub fn assertion_context(&self) -> String {
226 self.assertion_cx.context()
227 }
228
229 // unlike cx.simulate_keystrokes(), this does not run_until_parked
230 // so you can use it to test detailed timing
231 pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
232 let keystroke = Keystroke::parse(keystroke_text).unwrap();
233 self.cx.dispatch_keystroke(self.window, keystroke);
234 }
235
236 pub fn run_until_parked(&mut self) {
237 self.cx.background_executor.run_until_parked();
238 }
239
240 #[track_caller]
241 pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
242 let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
243 assert_eq!(self.buffer_text(), unmarked_text);
244 ranges
245 }
246
247 pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
248 let ranges = self.ranges(marked_text);
249 let snapshot = self.editor.update_in(&mut self.cx, |editor, window, cx| {
250 editor.snapshot(window, cx)
251 });
252 ranges[0].start.to_display_point(&snapshot)
253 }
254
255 pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
256 let display_point = self.display_point(marked_text);
257 self.pixel_position_for(display_point)
258 }
259
260 pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
261 self.update_editor(|editor, window, cx| {
262 let newest_point = editor.selections.newest_display(cx).head();
263 let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
264 let line_height = editor
265 .style()
266 .unwrap()
267 .text
268 .line_height_in_pixels(window.rem_size());
269 let snapshot = editor.snapshot(window, cx);
270 let details = editor.text_layout_details(window);
271
272 let y = pixel_position.y
273 + line_height * (display_point.row().as_f32() - newest_point.row().as_f32());
274 let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
275 - snapshot.x_for_display_point(newest_point, &details);
276 Point::new(x, y)
277 })
278 }
279
280 // Returns anchors for the current buffer using `«` and `»`
281 pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
282 let ranges = self.ranges(marked_text);
283 let snapshot = self.buffer_snapshot();
284 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
285 }
286
287 pub fn set_diff_base(&mut self, diff_base: &str) {
288 self.cx.run_until_parked();
289 let fs = self.update_editor(|editor, _, cx| {
290 editor.project.as_ref().unwrap().read(cx).fs().as_fake()
291 });
292 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
293 fs.set_index_for_repo(
294 &Self::root_path().join(".git"),
295 &[(path.as_ref(), diff_base.to_string())],
296 );
297 self.cx.run_until_parked();
298 }
299
300 /// Change the editor's text and selections using a string containing
301 /// embedded range markers that represent the ranges and directions of
302 /// each selection.
303 ///
304 /// Returns a context handle so that assertion failures can print what
305 /// editor state was needed to cause the failure.
306 ///
307 /// See the `util::test::marked_text_ranges` function for more information.
308 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
309 let state_context = self.add_assertion_context(format!(
310 "Initial Editor State: \"{}\"",
311 marked_text.escape_debug()
312 ));
313 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
314 self.editor.update_in(&mut self.cx, |editor, window, cx| {
315 editor.set_text(unmarked_text, window, cx);
316 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
317 s.select_ranges(selection_ranges)
318 })
319 });
320 state_context
321 }
322
323 /// Only change the editor's selections
324 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
325 let state_context = self.add_assertion_context(format!(
326 "Initial Editor State: \"{}\"",
327 marked_text.escape_debug()
328 ));
329 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
330 self.editor.update_in(&mut self.cx, |editor, window, cx| {
331 assert_eq!(editor.text(cx), unmarked_text);
332 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
333 s.select_ranges(selection_ranges)
334 })
335 });
336 state_context
337 }
338
339 /// Assert about the text of the editor, the selections, and the expanded
340 /// diff hunks.
341 ///
342 /// Diff hunks are indicated by lines starting with `+` and `-`.
343 #[track_caller]
344 pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
345 assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
346 }
347
348 /// Make an assertion about the editor's text and the ranges and directions
349 /// of its selections using a string containing embedded range markers.
350 ///
351 /// See the `util::test::marked_text_ranges` function for more information.
352 #[track_caller]
353 pub fn assert_editor_state(&mut self, marked_text: &str) {
354 let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
355 pretty_assertions::assert_eq!(self.buffer_text(), expected_text, "unexpected buffer text");
356 self.assert_selections(expected_selections, marked_text.to_string())
357 }
358
359 pub fn editor_state(&mut self) -> String {
360 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
361 }
362
363 #[track_caller]
364 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
365 let expected_ranges = self.ranges(marked_text);
366 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, window, cx| {
367 let snapshot = editor.snapshot(window, cx);
368 editor
369 .background_highlights
370 .get(&TypeId::of::<Tag>())
371 .map(|h| h.1.clone())
372 .unwrap_or_default()
373 .iter()
374 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
375 .collect()
376 });
377 assert_set_eq!(actual_ranges, expected_ranges);
378 }
379
380 #[track_caller]
381 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
382 let expected_ranges = self.ranges(marked_text);
383 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
384 let actual_ranges: Vec<Range<usize>> = snapshot
385 .text_highlight_ranges::<Tag>()
386 .map(|ranges| ranges.as_ref().clone().1)
387 .unwrap_or_default()
388 .into_iter()
389 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
390 .collect();
391 assert_set_eq!(actual_ranges, expected_ranges);
392 }
393
394 #[track_caller]
395 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
396 let expected_marked_text =
397 generate_marked_text(&self.buffer_text(), &expected_selections, true);
398 self.assert_selections(expected_selections, expected_marked_text)
399 }
400
401 #[track_caller]
402 fn editor_selections(&mut self) -> Vec<Range<usize>> {
403 self.editor
404 .update(&mut self.cx, |editor, cx| {
405 editor.selections.all::<usize>(cx)
406 })
407 .into_iter()
408 .map(|s| {
409 if s.reversed {
410 s.end..s.start
411 } else {
412 s.start..s.end
413 }
414 })
415 .collect::<Vec<_>>()
416 }
417
418 #[track_caller]
419 fn assert_selections(
420 &mut self,
421 expected_selections: Vec<Range<usize>>,
422 expected_marked_text: String,
423 ) {
424 let actual_selections = self.editor_selections();
425 let actual_marked_text =
426 generate_marked_text(&self.buffer_text(), &actual_selections, true);
427 if expected_selections != actual_selections {
428 pretty_assertions::assert_eq!(
429 actual_marked_text,
430 expected_marked_text,
431 "{}Editor has unexpected selections",
432 self.assertion_context(),
433 );
434 }
435 }
436}
437
438#[track_caller]
439pub fn assert_state_with_diff(
440 editor: &Entity<Editor>,
441 cx: &mut VisualTestContext,
442 expected_diff_text: &str,
443) {
444 let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| {
445 (
446 editor.snapshot(window, cx).buffer_snapshot.clone(),
447 editor.selections.ranges::<usize>(cx),
448 )
449 });
450
451 let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true);
452
453 // Read the actual diff.
454 let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
455 let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
456 let actual_diff = actual_marked_text
457 .split('\n')
458 .zip(line_infos)
459 .map(|(line, info)| {
460 let mut marker = match info.diff_status {
461 Some(DiffHunkStatus::Added) => "+ ",
462 Some(DiffHunkStatus::Removed) => "- ",
463 Some(DiffHunkStatus::Modified) => unreachable!(),
464 None => {
465 if has_diff {
466 " "
467 } else {
468 ""
469 }
470 }
471 };
472 if line.is_empty() {
473 marker = marker.trim();
474 }
475 format!("{marker}{line}")
476 })
477 .collect::<Vec<_>>()
478 .join("\n");
479
480 pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
481}
482
483impl Deref for EditorTestContext {
484 type Target = gpui::VisualTestContext;
485
486 fn deref(&self) -> &Self::Target {
487 &self.cx
488 }
489}
490
491impl DerefMut for EditorTestContext {
492 fn deref_mut(&mut self) -> &mut Self::Target {
493 &mut self.cx
494 }
495}
496
497/// Tracks string context to be printed when assertions fail.
498/// Often this is done by storing a context string in the manager and returning the handle.
499#[derive(Clone)]
500pub struct AssertionContextManager {
501 id: Arc<AtomicUsize>,
502 contexts: Arc<RwLock<BTreeMap<usize, String>>>,
503}
504
505impl Default for AssertionContextManager {
506 fn default() -> Self {
507 Self::new()
508 }
509}
510
511impl AssertionContextManager {
512 pub fn new() -> Self {
513 Self {
514 id: Arc::new(AtomicUsize::new(0)),
515 contexts: Arc::new(RwLock::new(BTreeMap::new())),
516 }
517 }
518
519 pub fn add_context(&self, context: String) -> ContextHandle {
520 let id = self.id.fetch_add(1, Ordering::Relaxed);
521 let mut contexts = self.contexts.write();
522 contexts.insert(id, context);
523 ContextHandle {
524 id,
525 manager: self.clone(),
526 }
527 }
528
529 pub fn context(&self) -> String {
530 let contexts = self.contexts.read();
531 format!("\n{}\n", contexts.values().join("\n"))
532 }
533}
534
535/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
536/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
537/// the state that was set initially for the failure can be printed in the error message
538pub struct ContextHandle {
539 id: usize,
540 manager: AssertionContextManager,
541}
542
543impl Drop for ContextHandle {
544 fn drop(&mut self) {
545 let mut contexts = self.manager.contexts.write();
546 contexts.remove(&self.id);
547 }
548}