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