1use crate::{
2 AnchorRangeExt, DisplayPoint, Editor, ExcerptId, MultiBuffer, MultiBufferSnapshot, RowExt,
3 display_map::{HighlightKey, ToDisplayPoint},
4};
5use buffer_diff::DiffHunkStatusKind;
6use collections::BTreeMap;
7use futures::Future;
8
9use gpui::{
10 AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point,
11 VisualTestContext, Window, WindowHandle, prelude::*,
12};
13use itertools::Itertools;
14use language::{Buffer, BufferSnapshot, LanguageRegistry};
15use multi_buffer::{Anchor, ExcerptRange, MultiBufferRow};
16use parking_lot::RwLock;
17use project::{FakeFs, Project};
18use std::{
19 any::TypeId,
20 ops::{Deref, DerefMut, Range},
21 path::Path,
22 sync::{
23 Arc,
24 atomic::{AtomicUsize, Ordering},
25 },
26};
27use text::Selection;
28use util::{
29 assert_set_eq,
30 test::{generate_marked_text, marked_text_ranges},
31};
32
33use super::{build_editor, build_editor_with_project};
34
35pub struct EditorTestContext {
36 pub cx: gpui::VisualTestContext,
37 pub window: AnyWindowHandle,
38 pub editor: Entity<Editor>,
39 pub assertion_cx: AssertionContextManager,
40}
41
42impl EditorTestContext {
43 pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
44 let fs = FakeFs::new(cx.executor());
45 let root = Self::root_path();
46 fs.insert_tree(
47 root,
48 serde_json::json!({
49 ".git": {},
50 "file": "",
51 }),
52 )
53 .await;
54 let project = Project::test(fs.clone(), [root], cx).await;
55 let buffer = project
56 .update(cx, |project, cx| {
57 project.open_local_buffer(root.join("file"), cx)
58 })
59 .await
60 .unwrap();
61
62 let language = project
63 .read_with(cx, |project, _cx| {
64 project.languages().language_for_name("Plain Text")
65 })
66 .await
67 .unwrap();
68 buffer.update(cx, |buffer, cx| {
69 buffer.set_language(Some(language), cx);
70 });
71
72 let editor = cx.add_window(|window, cx| {
73 let editor = build_editor_with_project(
74 project,
75 MultiBuffer::build_from_buffer(buffer, cx),
76 window,
77 cx,
78 );
79
80 window.focus(&editor.focus_handle(cx));
81 editor
82 });
83 let editor_view = editor.root(cx).unwrap();
84
85 cx.run_until_parked();
86 Self {
87 cx: VisualTestContext::from_window(*editor.deref(), cx),
88 window: editor.into(),
89 editor: editor_view,
90 assertion_cx: AssertionContextManager::new(),
91 }
92 }
93
94 #[cfg(target_os = "windows")]
95 fn root_path() -> &'static Path {
96 Path::new("C:\\root")
97 }
98
99 #[cfg(not(target_os = "windows"))]
100 fn root_path() -> &'static Path {
101 Path::new("/root")
102 }
103
104 pub async fn for_editor_in(editor: Entity<Editor>, cx: &mut gpui::VisualTestContext) -> Self {
105 cx.focus(&editor);
106 Self {
107 window: cx.windows()[0],
108 cx: cx.clone(),
109 editor,
110 assertion_cx: AssertionContextManager::new(),
111 }
112 }
113
114 pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
115 let editor_view = editor.root(cx).unwrap();
116 Self {
117 cx: VisualTestContext::from_window(*editor.deref(), cx),
118 window: editor.into(),
119 editor: editor_view,
120 assertion_cx: AssertionContextManager::new(),
121 }
122 }
123
124 #[track_caller]
125 pub fn new_multibuffer<const COUNT: usize>(
126 cx: &mut gpui::TestAppContext,
127 excerpts: [&str; COUNT],
128 ) -> EditorTestContext {
129 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
130 let buffer = cx.new(|cx| {
131 for excerpt in excerpts.into_iter() {
132 let (text, ranges) = marked_text_ranges(excerpt, false);
133 let buffer = cx.new(|cx| Buffer::local(text, cx));
134 multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx);
135 }
136 multibuffer
137 });
138
139 let editor = cx.add_window(|window, cx| {
140 let editor = build_editor(buffer, window, cx);
141 window.focus(&editor.focus_handle(cx));
142
143 editor
144 });
145
146 let editor_view = editor.root(cx).unwrap();
147 Self {
148 cx: VisualTestContext::from_window(*editor.deref(), cx),
149 window: editor.into(),
150 editor: editor_view,
151 assertion_cx: AssertionContextManager::new(),
152 }
153 }
154
155 pub fn condition(
156 &self,
157 predicate: impl FnMut(&Editor, &App) -> bool,
158 ) -> impl Future<Output = ()> {
159 self.editor
160 .condition::<crate::EditorEvent>(&self.cx, predicate)
161 }
162
163 #[track_caller]
164 pub fn editor<F, T>(&mut self, read: F) -> T
165 where
166 F: FnOnce(&Editor, &Window, &mut Context<Editor>) -> T,
167 {
168 self.editor
169 .update_in(&mut self.cx, |this, window, cx| read(this, window, cx))
170 }
171
172 #[track_caller]
173 pub fn update_editor<F, T>(&mut self, update: F) -> T
174 where
175 F: FnOnce(&mut Editor, &mut Window, &mut Context<Editor>) -> T,
176 {
177 self.editor.update_in(&mut self.cx, update)
178 }
179
180 pub fn multibuffer<F, T>(&mut self, read: F) -> T
181 where
182 F: FnOnce(&MultiBuffer, &App) -> T,
183 {
184 self.editor(|editor, _, cx| read(editor.buffer().read(cx), cx))
185 }
186
187 pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
188 where
189 F: FnOnce(&mut MultiBuffer, &mut Context<MultiBuffer>) -> T,
190 {
191 self.update_editor(|editor, _, cx| editor.buffer().update(cx, update))
192 }
193
194 pub fn buffer_text(&mut self) -> String {
195 self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
196 }
197
198 pub fn display_text(&mut self) -> String {
199 self.update_editor(|editor, _, cx| editor.display_text(cx))
200 }
201
202 pub fn buffer<F, T>(&mut self, read: F) -> T
203 where
204 F: FnOnce(&Buffer, &App) -> T,
205 {
206 self.multibuffer(|multibuffer, cx| {
207 let buffer = multibuffer.as_singleton().unwrap().read(cx);
208 read(buffer, cx)
209 })
210 }
211
212 pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
213 self.editor(|editor, _, cx| {
214 editor
215 .project
216 .as_ref()
217 .unwrap()
218 .read(cx)
219 .languages()
220 .clone()
221 })
222 }
223
224 pub fn update_buffer<F, T>(&mut self, update: F) -> T
225 where
226 F: FnOnce(&mut Buffer, &mut Context<Buffer>) -> T,
227 {
228 self.update_multibuffer(|multibuffer, cx| {
229 let buffer = multibuffer.as_singleton().unwrap();
230 buffer.update(cx, update)
231 })
232 }
233
234 pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
235 self.buffer(|buffer, _| buffer.snapshot())
236 }
237
238 pub fn add_assertion_context(&self, context: String) -> ContextHandle {
239 self.assertion_cx.add_context(context)
240 }
241
242 pub fn assertion_context(&self) -> String {
243 self.assertion_cx.context()
244 }
245
246 // unlike cx.simulate_keystrokes(), this does not run_until_parked
247 // so you can use it to test detailed timing
248 pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
249 let keystroke = Keystroke::parse(keystroke_text).unwrap();
250 self.cx.dispatch_keystroke(self.window, keystroke);
251 }
252
253 pub fn run_until_parked(&mut self) {
254 self.cx.background_executor.run_until_parked();
255 }
256
257 #[track_caller]
258 pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
259 let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
260 assert_eq!(self.buffer_text(), unmarked_text);
261 ranges
262 }
263
264 pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
265 let ranges = self.ranges(marked_text);
266 let snapshot = self.editor.update_in(&mut self.cx, |editor, window, cx| {
267 editor.snapshot(window, cx)
268 });
269 ranges[0].start.to_display_point(&snapshot)
270 }
271
272 pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
273 let display_point = self.display_point(marked_text);
274 self.pixel_position_for(display_point)
275 }
276
277 pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
278 self.update_editor(|editor, window, cx| {
279 let newest_point = editor
280 .selections
281 .newest_display(&editor.display_snapshot(cx))
282 .head();
283 let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
284 let line_height = editor
285 .style()
286 .unwrap()
287 .text
288 .line_height_in_pixels(window.rem_size());
289 let snapshot = editor.snapshot(window, cx);
290 let details = editor.text_layout_details(window);
291
292 let y = pixel_position.y
293 + f32::from(line_height)
294 * Pixels::from(display_point.row().as_f64() - newest_point.row().as_f64());
295 let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
296 - snapshot.x_for_display_point(newest_point, &details);
297 Point::new(x, y)
298 })
299 }
300
301 // Returns anchors for the current buffer using `«` and `»`
302 pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
303 let ranges = self.ranges(marked_text);
304 let snapshot = self.buffer_snapshot();
305 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
306 }
307
308 pub fn set_head_text(&mut self, diff_base: &str) {
309 self.cx.run_until_parked();
310 let fs =
311 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
312 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
313 fs.set_head_for_repo(
314 &Self::root_path().join(".git"),
315 &[(path.as_unix_str(), diff_base.to_string())],
316 "deadbeef",
317 );
318 self.cx.run_until_parked();
319 }
320
321 pub fn clear_index_text(&mut self) {
322 self.cx.run_until_parked();
323 let fs =
324 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
325 fs.set_index_for_repo(&Self::root_path().join(".git"), &[]);
326 self.cx.run_until_parked();
327 }
328
329 pub fn set_index_text(&mut self, diff_base: &str) {
330 self.cx.run_until_parked();
331 let fs =
332 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
333 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
334 fs.set_index_for_repo(
335 &Self::root_path().join(".git"),
336 &[(path.as_unix_str(), diff_base.to_string())],
337 );
338 self.cx.run_until_parked();
339 }
340
341 #[track_caller]
342 pub fn assert_index_text(&mut self, expected: Option<&str>) {
343 let fs =
344 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
345 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
346 let mut found = None;
347 fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
348 found = git_state.index_contents.get(&path.into()).cloned();
349 })
350 .unwrap();
351 assert_eq!(expected, found.as_deref());
352 }
353
354 /// Change the editor's text and selections using a string containing
355 /// embedded range markers that represent the ranges and directions of
356 /// each selection.
357 ///
358 /// Returns a context handle so that assertion failures can print what
359 /// editor state was needed to cause the failure.
360 ///
361 /// See the `util::test::marked_text_ranges` function for more information.
362 #[track_caller]
363 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
364 let state_context = self.add_assertion_context(format!(
365 "Initial Editor State: \"{}\"",
366 marked_text.escape_debug()
367 ));
368 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
369 self.editor.update_in(&mut self.cx, |editor, window, cx| {
370 editor.set_text(unmarked_text, window, cx);
371 editor.change_selections(Default::default(), window, cx, |s| {
372 s.select_ranges(selection_ranges)
373 })
374 });
375 state_context
376 }
377
378 /// Only change the editor's selections
379 #[track_caller]
380 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
381 let state_context = self.add_assertion_context(format!(
382 "Initial Editor State: \"{}\"",
383 marked_text.escape_debug()
384 ));
385 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
386 self.editor.update_in(&mut self.cx, |editor, window, cx| {
387 assert_eq!(editor.text(cx), unmarked_text);
388 editor.change_selections(Default::default(), window, cx, |s| {
389 s.select_ranges(selection_ranges)
390 })
391 });
392 state_context
393 }
394
395 /// Assert about the text of the editor, the selections, and the expanded
396 /// diff hunks.
397 ///
398 /// Diff hunks are indicated by lines starting with `+` and `-`.
399 #[track_caller]
400 pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
401 assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
402 }
403
404 #[track_caller]
405 pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
406 let actual_text = self.to_format_multibuffer_as_marked_text();
407 let fmt_additional_notes = || {
408 struct Format<'a, T: std::fmt::Display>(&'a str, &'a T);
409
410 impl<T: std::fmt::Display> std::fmt::Display for Format<'_, T> {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 write!(
413 f,
414 "\n\n----- EXPECTED: -----\n\n{}\n\n----- ACTUAL: -----\n\n{}\n\n",
415 self.0, self.1
416 )
417 }
418 }
419
420 Format(marked_text, &actual_text)
421 };
422
423 let expected_excerpts = marked_text
424 .strip_prefix("[EXCERPT]\n")
425 .unwrap()
426 .split("[EXCERPT]\n")
427 .collect::<Vec<_>>();
428
429 let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
430 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
431
432 let selections = editor.selections.disjoint_anchors_arc();
433 let excerpts = multibuffer_snapshot
434 .excerpts()
435 .map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
436 .collect::<Vec<_>>();
437
438 (multibuffer_snapshot, selections, excerpts)
439 });
440
441 assert!(
442 excerpts.len() == expected_excerpts.len(),
443 "should have {} excerpts, got {}{}",
444 expected_excerpts.len(),
445 excerpts.len(),
446 fmt_additional_notes(),
447 );
448
449 for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
450 let is_folded = self
451 .update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
452 let (expected_text, expected_selections) =
453 marked_text_ranges(expected_excerpts[ix], true);
454 if expected_text == "[FOLDED]\n" {
455 assert!(is_folded, "excerpt {} should be folded", ix);
456 let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
457 if !expected_selections.is_empty() {
458 assert!(
459 is_selected,
460 "excerpt {ix} should contain selections. got {:?}{}",
461 self.editor_state(),
462 fmt_additional_notes(),
463 );
464 } else {
465 assert!(
466 !is_selected,
467 "excerpt {ix} should not contain selections, got: {selections:?}{}",
468 fmt_additional_notes(),
469 );
470 }
471 continue;
472 }
473 assert!(
474 !is_folded,
475 "excerpt {} should not be folded{}",
476 ix,
477 fmt_additional_notes()
478 );
479 assert_eq!(
480 multibuffer_snapshot
481 .text_for_range(Anchor::range_in_buffer(
482 excerpt_id,
483 snapshot.remote_id(),
484 range.context.clone()
485 ))
486 .collect::<String>(),
487 expected_text,
488 "{}",
489 fmt_additional_notes(),
490 );
491
492 let selections = selections
493 .iter()
494 .filter(|s| s.head().excerpt_id == excerpt_id)
495 .map(|s| {
496 let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
497 - text::ToOffset::to_offset(&range.context.start, &snapshot);
498 let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
499 - text::ToOffset::to_offset(&range.context.start, &snapshot);
500 tail..head
501 })
502 .collect::<Vec<_>>();
503 // todo: selections that cross excerpt boundaries..
504 assert_eq!(
505 selections,
506 expected_selections,
507 "excerpt {} has incorrect selections{}",
508 ix,
509 fmt_additional_notes()
510 );
511 }
512 }
513
514 fn to_format_multibuffer_as_marked_text(&mut self) -> FormatMultiBufferAsMarkedText {
515 let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
516 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
517
518 let selections = editor.selections.disjoint_anchors_arc().to_vec();
519 let excerpts = multibuffer_snapshot
520 .excerpts()
521 .map(|(e_id, snapshot, range)| {
522 let is_folded = editor.is_buffer_folded(snapshot.remote_id(), cx);
523 (e_id, snapshot.clone(), range, is_folded)
524 })
525 .collect::<Vec<_>>();
526
527 (multibuffer_snapshot, selections, excerpts)
528 });
529
530 FormatMultiBufferAsMarkedText {
531 multibuffer_snapshot,
532 selections,
533 excerpts,
534 }
535 }
536
537 /// Make an assertion about the editor's text and the ranges and directions
538 /// of its selections using a string containing embedded range markers.
539 ///
540 /// See the `util::test::marked_text_ranges` function for more information.
541 #[track_caller]
542 pub fn assert_editor_state(&mut self, marked_text: &str) {
543 let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
544 pretty_assertions::assert_eq!(self.buffer_text(), expected_text, "unexpected buffer text");
545 self.assert_selections(expected_selections, marked_text.to_string())
546 }
547
548 /// Make an assertion about the editor's text and the ranges and directions
549 /// of its selections using a string containing embedded range markers.
550 ///
551 /// See the `util::test::marked_text_ranges` function for more information.
552 #[track_caller]
553 pub fn assert_display_state(&mut self, marked_text: &str) {
554 let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
555 pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
556 self.assert_selections(expected_selections, marked_text.to_string())
557 }
558
559 pub fn editor_state(&mut self) -> String {
560 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
561 }
562
563 #[track_caller]
564 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
565 let expected_ranges = self.ranges(marked_text);
566 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, window, cx| {
567 let snapshot = editor.snapshot(window, cx);
568 editor
569 .background_highlights
570 .get(&HighlightKey::Type(TypeId::of::<Tag>()))
571 .map(|h| h.1.clone())
572 .unwrap_or_default()
573 .iter()
574 .map(|range| range.to_offset(&snapshot.buffer_snapshot()))
575 .collect()
576 });
577 assert_set_eq!(actual_ranges, expected_ranges);
578 }
579
580 #[track_caller]
581 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
582 let expected_ranges = self.ranges(marked_text);
583 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
584 let actual_ranges: Vec<Range<usize>> = snapshot
585 .text_highlight_ranges::<Tag>()
586 .map(|ranges| ranges.as_ref().clone().1)
587 .unwrap_or_default()
588 .into_iter()
589 .map(|range| range.to_offset(&snapshot.buffer_snapshot()))
590 .collect();
591 assert_set_eq!(actual_ranges, expected_ranges);
592 }
593
594 #[track_caller]
595 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
596 let expected_marked_text =
597 generate_marked_text(&self.buffer_text(), &expected_selections, true)
598 .replace(" \n", "•\n");
599
600 self.assert_selections(expected_selections, expected_marked_text)
601 }
602
603 #[track_caller]
604 fn editor_selections(&mut self) -> Vec<Range<usize>> {
605 self.editor
606 .update(&mut self.cx, |editor, cx| {
607 editor.selections.all::<usize>(&editor.display_snapshot(cx))
608 })
609 .into_iter()
610 .map(|s| {
611 if s.reversed {
612 s.end..s.start
613 } else {
614 s.start..s.end
615 }
616 })
617 .collect::<Vec<_>>()
618 }
619
620 #[track_caller]
621 fn assert_selections(
622 &mut self,
623 expected_selections: Vec<Range<usize>>,
624 expected_marked_text: String,
625 ) {
626 let actual_selections = self.editor_selections();
627 let actual_marked_text =
628 generate_marked_text(&self.buffer_text(), &actual_selections, true)
629 .replace(" \n", "•\n");
630 if expected_selections != actual_selections {
631 pretty_assertions::assert_eq!(
632 actual_marked_text,
633 expected_marked_text,
634 "{}Editor has unexpected selections",
635 self.assertion_context(),
636 );
637 }
638 }
639}
640
641struct FormatMultiBufferAsMarkedText {
642 multibuffer_snapshot: MultiBufferSnapshot,
643 selections: Vec<Selection<Anchor>>,
644 excerpts: Vec<(ExcerptId, BufferSnapshot, ExcerptRange<text::Anchor>, bool)>,
645}
646
647impl std::fmt::Display for FormatMultiBufferAsMarkedText {
648 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649 let Self {
650 multibuffer_snapshot,
651 selections,
652 excerpts,
653 } = self;
654
655 for (excerpt_id, snapshot, range, is_folded) in excerpts.into_iter() {
656 write!(f, "[EXCERPT]\n")?;
657 if *is_folded {
658 write!(f, "[FOLDED]\n")?;
659 }
660
661 let mut text = multibuffer_snapshot
662 .text_for_range(Anchor::range_in_buffer(
663 *excerpt_id,
664 snapshot.remote_id(),
665 range.context.clone(),
666 ))
667 .collect::<String>();
668
669 let selections = selections
670 .iter()
671 .filter(|&s| s.head().excerpt_id == *excerpt_id)
672 .map(|s| {
673 let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
674 - text::ToOffset::to_offset(&range.context.start, &snapshot);
675 let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
676 - text::ToOffset::to_offset(&range.context.start, &snapshot);
677 tail..head
678 })
679 .rev()
680 .collect::<Vec<_>>();
681
682 for selection in selections {
683 if selection.is_empty() {
684 text.insert(selection.start, 'ˇ');
685 continue;
686 }
687 text.insert(selection.end, '»');
688 text.insert(selection.start, '«');
689 }
690
691 write!(f, "{text}")?;
692 }
693
694 Ok(())
695 }
696}
697
698#[track_caller]
699pub fn assert_state_with_diff(
700 editor: &Entity<Editor>,
701 cx: &mut VisualTestContext,
702 expected_diff_text: &str,
703) {
704 let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| {
705 let snapshot = editor.snapshot(window, cx);
706 (
707 snapshot.buffer_snapshot().clone(),
708 editor
709 .selections
710 .ranges::<usize>(&snapshot.display_snapshot),
711 )
712 });
713
714 let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true);
715
716 // Read the actual diff.
717 let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
718 let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
719 let actual_diff = actual_marked_text
720 .split('\n')
721 .zip(line_infos)
722 .map(|(line, info)| {
723 let mut marker = match info.diff_status.map(|status| status.kind) {
724 Some(DiffHunkStatusKind::Added) => "+ ",
725 Some(DiffHunkStatusKind::Deleted) => "- ",
726 Some(DiffHunkStatusKind::Modified) => unreachable!(),
727 None => {
728 if has_diff {
729 " "
730 } else {
731 ""
732 }
733 }
734 };
735 if line.is_empty() {
736 marker = marker.trim();
737 }
738 format!("{marker}{line}")
739 })
740 .collect::<Vec<_>>()
741 .join("\n");
742
743 pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
744}
745
746impl Deref for EditorTestContext {
747 type Target = gpui::VisualTestContext;
748
749 fn deref(&self) -> &Self::Target {
750 &self.cx
751 }
752}
753
754impl DerefMut for EditorTestContext {
755 fn deref_mut(&mut self) -> &mut Self::Target {
756 &mut self.cx
757 }
758}
759
760/// Tracks string context to be printed when assertions fail.
761/// Often this is done by storing a context string in the manager and returning the handle.
762#[derive(Clone)]
763pub struct AssertionContextManager {
764 id: Arc<AtomicUsize>,
765 contexts: Arc<RwLock<BTreeMap<usize, String>>>,
766}
767
768impl Default for AssertionContextManager {
769 fn default() -> Self {
770 Self::new()
771 }
772}
773
774impl AssertionContextManager {
775 pub fn new() -> Self {
776 Self {
777 id: Arc::new(AtomicUsize::new(0)),
778 contexts: Arc::new(RwLock::new(BTreeMap::new())),
779 }
780 }
781
782 pub fn add_context(&self, context: String) -> ContextHandle {
783 let id = self.id.fetch_add(1, Ordering::Relaxed);
784 let mut contexts = self.contexts.write();
785 contexts.insert(id, context);
786 ContextHandle {
787 id,
788 manager: self.clone(),
789 }
790 }
791
792 pub fn context(&self) -> String {
793 let contexts = self.contexts.read();
794 format!("\n{}\n", contexts.values().join("\n"))
795 }
796}
797
798/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
799/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
800/// the state that was set initially for the failure can be printed in the error message
801pub struct ContextHandle {
802 id: usize,
803 manager: AssertionContextManager,
804}
805
806impl Drop for ContextHandle {
807 fn drop(&mut self) {
808 let mut contexts = self.manager.contexts.write();
809 contexts.remove(&self.id);
810 }
811}