editor_test_context.rs

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