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