editor_test_context.rs

  1use crate::{
  2    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
  3    RowExt,
  4};
  5use collections::BTreeMap;
  6use futures::Future;
  7use git::diff::DiffHunkStatus;
  8use gpui::{
  9    prelude::*, AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point,
 10    VisualTestContext, Window, WindowHandle,
 11};
 12use itertools::Itertools;
 13use language::{Buffer, BufferSnapshot, LanguageRegistry};
 14use multi_buffer::{ExcerptRange, MultiBufferRow};
 15use parking_lot::RwLock;
 16use project::{FakeFs, Project};
 17use std::{
 18    any::TypeId,
 19    ops::{Deref, DerefMut, Range},
 20    path::Path,
 21    sync::{
 22        atomic::{AtomicUsize, Ordering},
 23        Arc,
 24    },
 25};
 26use util::{
 27    assert_set_eq,
 28    test::{generate_marked_text, marked_text_ranges},
 29};
 30
 31use super::{build_editor, build_editor_with_project};
 32
 33pub struct EditorTestContext {
 34    pub cx: gpui::VisualTestContext,
 35    pub window: AnyWindowHandle,
 36    pub editor: Entity<Editor>,
 37    pub assertion_cx: AssertionContextManager,
 38}
 39
 40impl EditorTestContext {
 41    pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
 42        let fs = FakeFs::new(cx.executor());
 43        let root = Self::root_path();
 44        fs.insert_tree(
 45            root,
 46            serde_json::json!({
 47                ".git": {},
 48                "file": "",
 49            }),
 50        )
 51        .await;
 52        let project = Project::test(fs.clone(), [root], cx).await;
 53        let buffer = project
 54            .update(cx, |project, cx| {
 55                project.open_local_buffer(root.join("file"), cx)
 56            })
 57            .await
 58            .unwrap();
 59        let editor = cx.add_window(|window, cx| {
 60            let editor = build_editor_with_project(
 61                project,
 62                MultiBuffer::build_from_buffer(buffer, cx),
 63                window,
 64                cx,
 65            );
 66
 67            window.focus(&editor.focus_handle(cx));
 68            editor
 69        });
 70        let editor_view = editor.root(cx).unwrap();
 71
 72        cx.run_until_parked();
 73        Self {
 74            cx: VisualTestContext::from_window(*editor.deref(), cx),
 75            window: editor.into(),
 76            editor: editor_view,
 77            assertion_cx: AssertionContextManager::new(),
 78        }
 79    }
 80
 81    #[cfg(target_os = "windows")]
 82    fn root_path() -> &'static Path {
 83        Path::new("C:\\root")
 84    }
 85
 86    #[cfg(not(target_os = "windows"))]
 87    fn root_path() -> &'static Path {
 88        Path::new("/root")
 89    }
 90
 91    pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
 92        let editor_view = editor.root(cx).unwrap();
 93        Self {
 94            cx: VisualTestContext::from_window(*editor.deref(), cx),
 95            window: editor.into(),
 96            editor: editor_view,
 97            assertion_cx: AssertionContextManager::new(),
 98        }
 99    }
100
101    pub fn new_multibuffer<const COUNT: usize>(
102        cx: &mut gpui::TestAppContext,
103        excerpts: [&str; COUNT],
104    ) -> EditorTestContext {
105        let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
106        let buffer = cx.new(|cx| {
107            for excerpt in excerpts.into_iter() {
108                let (text, ranges) = marked_text_ranges(excerpt, false);
109                let buffer = cx.new(|cx| Buffer::local(text, cx));
110                multibuffer.push_excerpts(
111                    buffer,
112                    ranges.into_iter().map(|range| ExcerptRange {
113                        context: range,
114                        primary: None,
115                    }),
116                    cx,
117                );
118            }
119            multibuffer
120        });
121
122        let editor = cx.add_window(|window, cx| {
123            let editor = build_editor(buffer, window, cx);
124            window.focus(&editor.focus_handle(cx));
125
126            editor
127        });
128
129        let editor_view = editor.root(cx).unwrap();
130        Self {
131            cx: VisualTestContext::from_window(*editor.deref(), cx),
132            window: editor.into(),
133            editor: editor_view,
134            assertion_cx: AssertionContextManager::new(),
135        }
136    }
137
138    pub fn condition(
139        &self,
140        predicate: impl FnMut(&Editor, &App) -> bool,
141    ) -> impl Future<Output = ()> {
142        self.editor
143            .condition::<crate::EditorEvent>(&self.cx, predicate)
144    }
145
146    #[track_caller]
147    pub fn editor<F, T>(&mut self, read: F) -> T
148    where
149        F: FnOnce(&Editor, &Window, &mut Context<Editor>) -> T,
150    {
151        self.editor
152            .update_in(&mut self.cx, |this, window, cx| read(this, window, cx))
153    }
154
155    #[track_caller]
156    pub fn update_editor<F, T>(&mut self, update: F) -> T
157    where
158        F: FnOnce(&mut Editor, &mut Window, &mut Context<Editor>) -> T,
159    {
160        self.editor.update_in(&mut self.cx, update)
161    }
162
163    pub fn multibuffer<F, T>(&mut self, read: F) -> T
164    where
165        F: FnOnce(&MultiBuffer, &App) -> T,
166    {
167        self.editor(|editor, _, cx| read(editor.buffer().read(cx), cx))
168    }
169
170    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
171    where
172        F: FnOnce(&mut MultiBuffer, &mut Context<MultiBuffer>) -> T,
173    {
174        self.update_editor(|editor, _, cx| editor.buffer().update(cx, update))
175    }
176
177    pub fn buffer_text(&mut self) -> String {
178        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
179    }
180
181    pub fn display_text(&mut self) -> String {
182        self.update_editor(|editor, _, cx| editor.display_text(cx))
183    }
184
185    pub fn buffer<F, T>(&mut self, read: F) -> T
186    where
187        F: FnOnce(&Buffer, &App) -> T,
188    {
189        self.multibuffer(|multibuffer, cx| {
190            let buffer = multibuffer.as_singleton().unwrap().read(cx);
191            read(buffer, cx)
192        })
193    }
194
195    pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
196        self.editor(|editor, _, cx| {
197            editor
198                .project
199                .as_ref()
200                .unwrap()
201                .read(cx)
202                .languages()
203                .clone()
204        })
205    }
206
207    pub fn update_buffer<F, T>(&mut self, update: F) -> T
208    where
209        F: FnOnce(&mut Buffer, &mut Context<Buffer>) -> T,
210    {
211        self.update_multibuffer(|multibuffer, cx| {
212            let buffer = multibuffer.as_singleton().unwrap();
213            buffer.update(cx, update)
214        })
215    }
216
217    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
218        self.buffer(|buffer, _| buffer.snapshot())
219    }
220
221    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
222        self.assertion_cx.add_context(context)
223    }
224
225    pub fn assertion_context(&self) -> String {
226        self.assertion_cx.context()
227    }
228
229    // unlike cx.simulate_keystrokes(), this does not run_until_parked
230    // so you can use it to test detailed timing
231    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
232        let keystroke = Keystroke::parse(keystroke_text).unwrap();
233        self.cx.dispatch_keystroke(self.window, keystroke);
234    }
235
236    pub fn run_until_parked(&mut self) {
237        self.cx.background_executor.run_until_parked();
238    }
239
240    #[track_caller]
241    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
242        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
243        assert_eq!(self.buffer_text(), unmarked_text);
244        ranges
245    }
246
247    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
248        let ranges = self.ranges(marked_text);
249        let snapshot = self.editor.update_in(&mut self.cx, |editor, window, cx| {
250            editor.snapshot(window, cx)
251        });
252        ranges[0].start.to_display_point(&snapshot)
253    }
254
255    pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
256        let display_point = self.display_point(marked_text);
257        self.pixel_position_for(display_point)
258    }
259
260    pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
261        self.update_editor(|editor, window, cx| {
262            let newest_point = editor.selections.newest_display(cx).head();
263            let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
264            let line_height = editor
265                .style()
266                .unwrap()
267                .text
268                .line_height_in_pixels(window.rem_size());
269            let snapshot = editor.snapshot(window, cx);
270            let details = editor.text_layout_details(window);
271
272            let y = pixel_position.y
273                + line_height * (display_point.row().as_f32() - newest_point.row().as_f32());
274            let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
275                - snapshot.x_for_display_point(newest_point, &details);
276            Point::new(x, y)
277        })
278    }
279
280    // Returns anchors for the current buffer using `«` and `»`
281    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
282        let ranges = self.ranges(marked_text);
283        let snapshot = self.buffer_snapshot();
284        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
285    }
286
287    pub fn set_diff_base(&mut self, diff_base: &str) {
288        self.cx.run_until_parked();
289        let fs = self.update_editor(|editor, _, cx| {
290            editor.project.as_ref().unwrap().read(cx).fs().as_fake()
291        });
292        let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
293        fs.set_index_for_repo(
294            &Self::root_path().join(".git"),
295            &[(path.as_ref(), diff_base.to_string())],
296        );
297        self.cx.run_until_parked();
298    }
299
300    /// Change the editor's text and selections using a string containing
301    /// embedded range markers that represent the ranges and directions of
302    /// each selection.
303    ///
304    /// Returns a context handle so that assertion failures can print what
305    /// editor state was needed to cause the failure.
306    ///
307    /// See the `util::test::marked_text_ranges` function for more information.
308    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
309        let state_context = self.add_assertion_context(format!(
310            "Initial Editor State: \"{}\"",
311            marked_text.escape_debug()
312        ));
313        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
314        self.editor.update_in(&mut self.cx, |editor, window, cx| {
315            editor.set_text(unmarked_text, window, cx);
316            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
317                s.select_ranges(selection_ranges)
318            })
319        });
320        state_context
321    }
322
323    /// Only change the editor's selections
324    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
325        let state_context = self.add_assertion_context(format!(
326            "Initial Editor State: \"{}\"",
327            marked_text.escape_debug()
328        ));
329        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
330        self.editor.update_in(&mut self.cx, |editor, window, cx| {
331            assert_eq!(editor.text(cx), unmarked_text);
332            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
333                s.select_ranges(selection_ranges)
334            })
335        });
336        state_context
337    }
338
339    /// Assert about the text of the editor, the selections, and the expanded
340    /// diff hunks.
341    ///
342    /// Diff hunks are indicated by lines starting with `+` and `-`.
343    #[track_caller]
344    pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
345        assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
346    }
347
348    /// Make an assertion about the editor's text and the ranges and directions
349    /// of its selections using a string containing embedded range markers.
350    ///
351    /// See the `util::test::marked_text_ranges` function for more information.
352    #[track_caller]
353    pub fn assert_editor_state(&mut self, marked_text: &str) {
354        let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
355        pretty_assertions::assert_eq!(self.buffer_text(), expected_text, "unexpected buffer text");
356        self.assert_selections(expected_selections, marked_text.to_string())
357    }
358
359    pub fn editor_state(&mut self) -> String {
360        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
361    }
362
363    #[track_caller]
364    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
365        let expected_ranges = self.ranges(marked_text);
366        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, window, cx| {
367            let snapshot = editor.snapshot(window, cx);
368            editor
369                .background_highlights
370                .get(&TypeId::of::<Tag>())
371                .map(|h| h.1.clone())
372                .unwrap_or_default()
373                .iter()
374                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
375                .collect()
376        });
377        assert_set_eq!(actual_ranges, expected_ranges);
378    }
379
380    #[track_caller]
381    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
382        let expected_ranges = self.ranges(marked_text);
383        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
384        let actual_ranges: Vec<Range<usize>> = snapshot
385            .text_highlight_ranges::<Tag>()
386            .map(|ranges| ranges.as_ref().clone().1)
387            .unwrap_or_default()
388            .into_iter()
389            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
390            .collect();
391        assert_set_eq!(actual_ranges, expected_ranges);
392    }
393
394    #[track_caller]
395    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
396        let expected_marked_text =
397            generate_marked_text(&self.buffer_text(), &expected_selections, true);
398        self.assert_selections(expected_selections, expected_marked_text)
399    }
400
401    #[track_caller]
402    fn editor_selections(&mut self) -> Vec<Range<usize>> {
403        self.editor
404            .update(&mut self.cx, |editor, cx| {
405                editor.selections.all::<usize>(cx)
406            })
407            .into_iter()
408            .map(|s| {
409                if s.reversed {
410                    s.end..s.start
411                } else {
412                    s.start..s.end
413                }
414            })
415            .collect::<Vec<_>>()
416    }
417
418    #[track_caller]
419    fn assert_selections(
420        &mut self,
421        expected_selections: Vec<Range<usize>>,
422        expected_marked_text: String,
423    ) {
424        let actual_selections = self.editor_selections();
425        let actual_marked_text =
426            generate_marked_text(&self.buffer_text(), &actual_selections, true);
427        if expected_selections != actual_selections {
428            pretty_assertions::assert_eq!(
429                actual_marked_text,
430                expected_marked_text,
431                "{}Editor has unexpected selections",
432                self.assertion_context(),
433            );
434        }
435    }
436}
437
438#[track_caller]
439pub fn assert_state_with_diff(
440    editor: &Entity<Editor>,
441    cx: &mut VisualTestContext,
442    expected_diff_text: &str,
443) {
444    let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| {
445        (
446            editor.snapshot(window, cx).buffer_snapshot.clone(),
447            editor.selections.ranges::<usize>(cx),
448        )
449    });
450
451    let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true);
452
453    // Read the actual diff.
454    let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
455    let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
456    let actual_diff = actual_marked_text
457        .split('\n')
458        .zip(line_infos)
459        .map(|(line, info)| {
460            let mut marker = match info.diff_status {
461                Some(DiffHunkStatus::Added) => "+ ",
462                Some(DiffHunkStatus::Removed) => "- ",
463                Some(DiffHunkStatus::Modified) => unreachable!(),
464                None => {
465                    if has_diff {
466                        "  "
467                    } else {
468                        ""
469                    }
470                }
471            };
472            if line.is_empty() {
473                marker = marker.trim();
474            }
475            format!("{marker}{line}")
476        })
477        .collect::<Vec<_>>()
478        .join("\n");
479
480    pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
481}
482
483impl Deref for EditorTestContext {
484    type Target = gpui::VisualTestContext;
485
486    fn deref(&self) -> &Self::Target {
487        &self.cx
488    }
489}
490
491impl DerefMut for EditorTestContext {
492    fn deref_mut(&mut self) -> &mut Self::Target {
493        &mut self.cx
494    }
495}
496
497/// Tracks string context to be printed when assertions fail.
498/// Often this is done by storing a context string in the manager and returning the handle.
499#[derive(Clone)]
500pub struct AssertionContextManager {
501    id: Arc<AtomicUsize>,
502    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
503}
504
505impl Default for AssertionContextManager {
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511impl AssertionContextManager {
512    pub fn new() -> Self {
513        Self {
514            id: Arc::new(AtomicUsize::new(0)),
515            contexts: Arc::new(RwLock::new(BTreeMap::new())),
516        }
517    }
518
519    pub fn add_context(&self, context: String) -> ContextHandle {
520        let id = self.id.fetch_add(1, Ordering::Relaxed);
521        let mut contexts = self.contexts.write();
522        contexts.insert(id, context);
523        ContextHandle {
524            id,
525            manager: self.clone(),
526        }
527    }
528
529    pub fn context(&self) -> String {
530        let contexts = self.contexts.read();
531        format!("\n{}\n", contexts.values().join("\n"))
532    }
533}
534
535/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
536/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
537/// the state that was set initially for the failure can be printed in the error message
538pub struct ContextHandle {
539    id: usize,
540    manager: AssertionContextManager,
541}
542
543impl Drop for ContextHandle {
544    fn drop(&mut self) {
545        let mut contexts = self.manager.contexts.write();
546        contexts.remove(&self.id);
547    }
548}