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.selections.newest_display(cx).head();
280 let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
281 let line_height = editor
282 .style()
283 .unwrap()
284 .text
285 .line_height_in_pixels(window.rem_size());
286 let snapshot = editor.snapshot(window, cx);
287 let details = editor.text_layout_details(window);
288
289 let y = pixel_position.y
290 + f32::from(line_height)
291 * Pixels::from(display_point.row().as_f64() - newest_point.row().as_f64());
292 let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
293 - snapshot.x_for_display_point(newest_point, &details);
294 Point::new(x, y)
295 })
296 }
297
298 // Returns anchors for the current buffer using `«` and `»`
299 pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
300 let ranges = self.ranges(marked_text);
301 let snapshot = self.buffer_snapshot();
302 snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
303 }
304
305 pub fn set_head_text(&mut self, diff_base: &str) {
306 self.cx.run_until_parked();
307 let fs =
308 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
309 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
310 fs.set_head_for_repo(
311 &Self::root_path().join(".git"),
312 &[(path.as_unix_str(), diff_base.to_string())],
313 "deadbeef",
314 );
315 self.cx.run_until_parked();
316 }
317
318 pub fn clear_index_text(&mut self) {
319 self.cx.run_until_parked();
320 let fs =
321 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
322 fs.set_index_for_repo(&Self::root_path().join(".git"), &[]);
323 self.cx.run_until_parked();
324 }
325
326 pub fn set_index_text(&mut self, diff_base: &str) {
327 self.cx.run_until_parked();
328 let fs =
329 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
330 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
331 fs.set_index_for_repo(
332 &Self::root_path().join(".git"),
333 &[(path.as_unix_str(), diff_base.to_string())],
334 );
335 self.cx.run_until_parked();
336 }
337
338 #[track_caller]
339 pub fn assert_index_text(&mut self, expected: Option<&str>) {
340 let fs =
341 self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
342 let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
343 let mut found = None;
344 fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
345 found = git_state.index_contents.get(&path.into()).cloned();
346 })
347 .unwrap();
348 assert_eq!(expected, found.as_deref());
349 }
350
351 /// Change the editor's text and selections using a string containing
352 /// embedded range markers that represent the ranges and directions of
353 /// each selection.
354 ///
355 /// Returns a context handle so that assertion failures can print what
356 /// editor state was needed to cause the failure.
357 ///
358 /// See the `util::test::marked_text_ranges` function for more information.
359 #[track_caller]
360 pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
361 let state_context = self.add_assertion_context(format!(
362 "Initial Editor State: \"{}\"",
363 marked_text.escape_debug()
364 ));
365 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
366 self.editor.update_in(&mut self.cx, |editor, window, cx| {
367 editor.set_text(unmarked_text, window, cx);
368 editor.change_selections(Default::default(), window, cx, |s| {
369 s.select_ranges(selection_ranges)
370 })
371 });
372 state_context
373 }
374
375 /// Only change the editor's selections
376 #[track_caller]
377 pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
378 let state_context = self.add_assertion_context(format!(
379 "Initial Editor State: \"{}\"",
380 marked_text.escape_debug()
381 ));
382 let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
383 self.editor.update_in(&mut self.cx, |editor, window, cx| {
384 assert_eq!(editor.text(cx), unmarked_text);
385 editor.change_selections(Default::default(), window, cx, |s| {
386 s.select_ranges(selection_ranges)
387 })
388 });
389 state_context
390 }
391
392 /// Assert about the text of the editor, the selections, and the expanded
393 /// diff hunks.
394 ///
395 /// Diff hunks are indicated by lines starting with `+` and `-`.
396 #[track_caller]
397 pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
398 assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
399 }
400
401 #[track_caller]
402 pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
403 let actual_text = self.to_format_multibuffer_as_marked_text();
404 let fmt_additional_notes = || {
405 struct Format<'a, T: std::fmt::Display>(&'a str, &'a T);
406
407 impl<T: std::fmt::Display> std::fmt::Display for Format<'_, T> {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 write!(
410 f,
411 "\n\n----- EXPECTED: -----\n\n{}\n\n----- ACTUAL: -----\n\n{}\n\n",
412 self.0, self.1
413 )
414 }
415 }
416
417 Format(marked_text, &actual_text)
418 };
419
420 let expected_excerpts = marked_text
421 .strip_prefix("[EXCERPT]\n")
422 .unwrap()
423 .split("[EXCERPT]\n")
424 .collect::<Vec<_>>();
425
426 let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
427 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
428
429 let selections = editor.selections.disjoint_anchors_arc();
430 let excerpts = multibuffer_snapshot
431 .excerpts()
432 .map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
433 .collect::<Vec<_>>();
434
435 (multibuffer_snapshot, selections, excerpts)
436 });
437
438 assert!(
439 excerpts.len() == expected_excerpts.len(),
440 "should have {} excerpts, got {}{}",
441 expected_excerpts.len(),
442 excerpts.len(),
443 fmt_additional_notes(),
444 );
445
446 for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
447 let is_folded = self
448 .update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
449 let (expected_text, expected_selections) =
450 marked_text_ranges(expected_excerpts[ix], true);
451 if expected_text == "[FOLDED]\n" {
452 assert!(is_folded, "excerpt {} should be folded", ix);
453 let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
454 if !expected_selections.is_empty() {
455 assert!(
456 is_selected,
457 "excerpt {ix} should contain selections. got {:?}{}",
458 self.editor_state(),
459 fmt_additional_notes(),
460 );
461 } else {
462 assert!(
463 !is_selected,
464 "excerpt {ix} should not contain selections, got: {selections:?}{}",
465 fmt_additional_notes(),
466 );
467 }
468 continue;
469 }
470 assert!(
471 !is_folded,
472 "excerpt {} should not be folded{}",
473 ix,
474 fmt_additional_notes()
475 );
476 assert_eq!(
477 multibuffer_snapshot
478 .text_for_range(Anchor::range_in_buffer(
479 excerpt_id,
480 snapshot.remote_id(),
481 range.context.clone()
482 ))
483 .collect::<String>(),
484 expected_text,
485 "{}",
486 fmt_additional_notes(),
487 );
488
489 let selections = selections
490 .iter()
491 .filter(|s| s.head().excerpt_id == excerpt_id)
492 .map(|s| {
493 let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
494 - text::ToOffset::to_offset(&range.context.start, &snapshot);
495 let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
496 - text::ToOffset::to_offset(&range.context.start, &snapshot);
497 tail..head
498 })
499 .collect::<Vec<_>>();
500 // todo: selections that cross excerpt boundaries..
501 assert_eq!(
502 selections,
503 expected_selections,
504 "excerpt {} has incorrect selections{}",
505 ix,
506 fmt_additional_notes()
507 );
508 }
509 }
510
511 fn to_format_multibuffer_as_marked_text(&mut self) -> FormatMultiBufferAsMarkedText {
512 let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
513 let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
514
515 let selections = editor.selections.disjoint_anchors_arc().to_vec();
516 let excerpts = multibuffer_snapshot
517 .excerpts()
518 .map(|(e_id, snapshot, range)| {
519 let is_folded = editor.is_buffer_folded(snapshot.remote_id(), cx);
520 (e_id, snapshot.clone(), range, is_folded)
521 })
522 .collect::<Vec<_>>();
523
524 (multibuffer_snapshot, selections, excerpts)
525 });
526
527 FormatMultiBufferAsMarkedText {
528 multibuffer_snapshot,
529 selections,
530 excerpts,
531 }
532 }
533
534 /// Make an assertion about the editor's text and the ranges and directions
535 /// of its selections using a string containing embedded range markers.
536 ///
537 /// See the `util::test::marked_text_ranges` function for more information.
538 #[track_caller]
539 pub fn assert_editor_state(&mut self, marked_text: &str) {
540 let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
541 pretty_assertions::assert_eq!(self.buffer_text(), expected_text, "unexpected buffer text");
542 self.assert_selections(expected_selections, marked_text.to_string())
543 }
544
545 /// Make an assertion about the editor's text and the ranges and directions
546 /// of its selections using a string containing embedded range markers.
547 ///
548 /// See the `util::test::marked_text_ranges` function for more information.
549 #[track_caller]
550 pub fn assert_display_state(&mut self, marked_text: &str) {
551 let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
552 pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
553 self.assert_selections(expected_selections, marked_text.to_string())
554 }
555
556 pub fn editor_state(&mut self) -> String {
557 generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
558 }
559
560 #[track_caller]
561 pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
562 let expected_ranges = self.ranges(marked_text);
563 let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, window, cx| {
564 let snapshot = editor.snapshot(window, cx);
565 editor
566 .background_highlights
567 .get(&HighlightKey::Type(TypeId::of::<Tag>()))
568 .map(|h| h.1.clone())
569 .unwrap_or_default()
570 .iter()
571 .map(|range| range.to_offset(&snapshot.buffer_snapshot()))
572 .collect()
573 });
574 assert_set_eq!(actual_ranges, expected_ranges);
575 }
576
577 #[track_caller]
578 pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
579 let expected_ranges = self.ranges(marked_text);
580 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
581 let actual_ranges: Vec<Range<usize>> = snapshot
582 .text_highlight_ranges::<Tag>()
583 .map(|ranges| ranges.as_ref().clone().1)
584 .unwrap_or_default()
585 .into_iter()
586 .map(|range| range.to_offset(&snapshot.buffer_snapshot()))
587 .collect();
588 assert_set_eq!(actual_ranges, expected_ranges);
589 }
590
591 #[track_caller]
592 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
593 let expected_marked_text =
594 generate_marked_text(&self.buffer_text(), &expected_selections, true)
595 .replace(" \n", "•\n");
596
597 self.assert_selections(expected_selections, expected_marked_text)
598 }
599
600 #[track_caller]
601 fn editor_selections(&mut self) -> Vec<Range<usize>> {
602 self.editor
603 .update(&mut self.cx, |editor, cx| {
604 editor.selections.all::<usize>(cx)
605 })
606 .into_iter()
607 .map(|s| {
608 if s.reversed {
609 s.end..s.start
610 } else {
611 s.start..s.end
612 }
613 })
614 .collect::<Vec<_>>()
615 }
616
617 #[track_caller]
618 fn assert_selections(
619 &mut self,
620 expected_selections: Vec<Range<usize>>,
621 expected_marked_text: String,
622 ) {
623 let actual_selections = self.editor_selections();
624 let actual_marked_text =
625 generate_marked_text(&self.buffer_text(), &actual_selections, true)
626 .replace(" \n", "•\n");
627 if expected_selections != actual_selections {
628 pretty_assertions::assert_eq!(
629 actual_marked_text,
630 expected_marked_text,
631 "{}Editor has unexpected selections",
632 self.assertion_context(),
633 );
634 }
635 }
636}
637
638struct FormatMultiBufferAsMarkedText {
639 multibuffer_snapshot: MultiBufferSnapshot,
640 selections: Vec<Selection<Anchor>>,
641 excerpts: Vec<(ExcerptId, BufferSnapshot, ExcerptRange<text::Anchor>, bool)>,
642}
643
644impl std::fmt::Display for FormatMultiBufferAsMarkedText {
645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646 let Self {
647 multibuffer_snapshot,
648 selections,
649 excerpts,
650 } = self;
651
652 for (excerpt_id, snapshot, range, is_folded) in excerpts.into_iter() {
653 write!(f, "[EXCERPT]\n")?;
654 if *is_folded {
655 write!(f, "[FOLDED]\n")?;
656 }
657
658 let mut text = multibuffer_snapshot
659 .text_for_range(Anchor::range_in_buffer(
660 *excerpt_id,
661 snapshot.remote_id(),
662 range.context.clone(),
663 ))
664 .collect::<String>();
665
666 let selections = selections
667 .iter()
668 .filter(|&s| s.head().excerpt_id == *excerpt_id)
669 .map(|s| {
670 let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
671 - text::ToOffset::to_offset(&range.context.start, &snapshot);
672 let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
673 - text::ToOffset::to_offset(&range.context.start, &snapshot);
674 tail..head
675 })
676 .rev()
677 .collect::<Vec<_>>();
678
679 for selection in selections {
680 if selection.is_empty() {
681 text.insert(selection.start, 'ˇ');
682 continue;
683 }
684 text.insert(selection.end, '»');
685 text.insert(selection.start, '«');
686 }
687
688 write!(f, "{text}")?;
689 }
690
691 Ok(())
692 }
693}
694
695#[track_caller]
696pub fn assert_state_with_diff(
697 editor: &Entity<Editor>,
698 cx: &mut VisualTestContext,
699 expected_diff_text: &str,
700) {
701 let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| {
702 (
703 editor.snapshot(window, cx).buffer_snapshot().clone(),
704 editor.selections.ranges::<usize>(cx),
705 )
706 });
707
708 let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true);
709
710 // Read the actual diff.
711 let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
712 let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
713 let actual_diff = actual_marked_text
714 .split('\n')
715 .zip(line_infos)
716 .map(|(line, info)| {
717 let mut marker = match info.diff_status.map(|status| status.kind) {
718 Some(DiffHunkStatusKind::Added) => "+ ",
719 Some(DiffHunkStatusKind::Deleted) => "- ",
720 Some(DiffHunkStatusKind::Modified) => unreachable!(),
721 None => {
722 if has_diff {
723 " "
724 } else {
725 ""
726 }
727 }
728 };
729 if line.is_empty() {
730 marker = marker.trim();
731 }
732 format!("{marker}{line}")
733 })
734 .collect::<Vec<_>>()
735 .join("\n");
736
737 pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
738}
739
740impl Deref for EditorTestContext {
741 type Target = gpui::VisualTestContext;
742
743 fn deref(&self) -> &Self::Target {
744 &self.cx
745 }
746}
747
748impl DerefMut for EditorTestContext {
749 fn deref_mut(&mut self) -> &mut Self::Target {
750 &mut self.cx
751 }
752}
753
754/// Tracks string context to be printed when assertions fail.
755/// Often this is done by storing a context string in the manager and returning the handle.
756#[derive(Clone)]
757pub struct AssertionContextManager {
758 id: Arc<AtomicUsize>,
759 contexts: Arc<RwLock<BTreeMap<usize, String>>>,
760}
761
762impl Default for AssertionContextManager {
763 fn default() -> Self {
764 Self::new()
765 }
766}
767
768impl AssertionContextManager {
769 pub fn new() -> Self {
770 Self {
771 id: Arc::new(AtomicUsize::new(0)),
772 contexts: Arc::new(RwLock::new(BTreeMap::new())),
773 }
774 }
775
776 pub fn add_context(&self, context: String) -> ContextHandle {
777 let id = self.id.fetch_add(1, Ordering::Relaxed);
778 let mut contexts = self.contexts.write();
779 contexts.insert(id, context);
780 ContextHandle {
781 id,
782 manager: self.clone(),
783 }
784 }
785
786 pub fn context(&self) -> String {
787 let contexts = self.contexts.read();
788 format!("\n{}\n", contexts.values().join("\n"))
789 }
790}
791
792/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
793/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
794/// the state that was set initially for the failure can be printed in the error message
795pub struct ContextHandle {
796 id: usize,
797 manager: AssertionContextManager,
798}
799
800impl Drop for ContextHandle {
801 fn drop(&mut self) {
802 let mut contexts = self.manager.contexts.write();
803 contexts.remove(&self.id);
804 }
805}