editor_test_context.rs

  1use crate::{
  2    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
  3};
  4use collections::BTreeMap;
  5use futures::Future;
  6use gpui::{
  7    AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
  8    VisualTestContext,
  9};
 10use indoc::indoc;
 11use itertools::Itertools;
 12use language::{Buffer, BufferSnapshot, LanguageRegistry};
 13use parking_lot::RwLock;
 14use project::{FakeFs, Project};
 15use std::{
 16    any::TypeId,
 17    ops::{Deref, DerefMut, Range},
 18    sync::{
 19        atomic::{AtomicUsize, Ordering},
 20        Arc,
 21    },
 22};
 23use util::{
 24    assert_set_eq,
 25    test::{generate_marked_text, marked_text_ranges},
 26};
 27
 28use super::build_editor_with_project;
 29
 30pub struct EditorTestContext {
 31    pub cx: gpui::VisualTestContext,
 32    pub window: AnyWindowHandle,
 33    pub editor: View<Editor>,
 34    pub assertion_cx: AssertionContextManager,
 35}
 36
 37impl EditorTestContext {
 38    pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
 39        let fs = FakeFs::new(cx.executor());
 40        // fs.insert_file("/file", "".to_owned()).await;
 41        fs.insert_tree(
 42            "/root",
 43            serde_json::json!({
 44                "file": "",
 45            }),
 46        )
 47        .await;
 48        let project = Project::test(fs, ["/root".as_ref()], cx).await;
 49        let buffer = project
 50            .update(cx, |project, cx| {
 51                project.open_local_buffer("/root/file", cx)
 52            })
 53            .await
 54            .unwrap();
 55        let editor = cx.add_window(|cx| {
 56            let editor =
 57                build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
 58            editor.focus(cx);
 59            editor
 60        });
 61        let editor_view = editor.root_view(cx).unwrap();
 62        Self {
 63            cx: VisualTestContext::from_window(*editor.deref(), cx),
 64            window: editor.into(),
 65            editor: editor_view,
 66            assertion_cx: AssertionContextManager::new(),
 67        }
 68    }
 69
 70    pub fn condition(
 71        &self,
 72        predicate: impl FnMut(&Editor, &AppContext) -> bool,
 73    ) -> impl Future<Output = ()> {
 74        self.editor
 75            .condition::<crate::EditorEvent>(&self.cx, predicate)
 76    }
 77
 78    #[track_caller]
 79    pub fn editor<F, T>(&mut self, read: F) -> T
 80    where
 81        F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
 82    {
 83        self.editor
 84            .update(&mut self.cx, |this, cx| read(&this, &cx))
 85    }
 86
 87    #[track_caller]
 88    pub fn update_editor<F, T>(&mut self, update: F) -> T
 89    where
 90        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
 91    {
 92        self.editor.update(&mut self.cx, update)
 93    }
 94
 95    pub fn multibuffer<F, T>(&mut self, read: F) -> T
 96    where
 97        F: FnOnce(&MultiBuffer, &AppContext) -> T,
 98    {
 99        self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
100    }
101
102    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
103    where
104        F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
105    {
106        self.update_editor(|editor, cx| editor.buffer().update(cx, update))
107    }
108
109    pub fn buffer_text(&mut self) -> String {
110        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
111    }
112
113    pub fn buffer<F, T>(&mut self, read: F) -> T
114    where
115        F: FnOnce(&Buffer, &AppContext) -> T,
116    {
117        self.multibuffer(|multibuffer, cx| {
118            let buffer = multibuffer.as_singleton().unwrap().read(cx);
119            read(buffer, cx)
120        })
121    }
122
123    pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
124        self.editor(|editor, cx| {
125            editor
126                .project
127                .as_ref()
128                .unwrap()
129                .read(cx)
130                .languages()
131                .clone()
132        })
133    }
134
135    pub fn update_buffer<F, T>(&mut self, update: F) -> T
136    where
137        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
138    {
139        self.update_multibuffer(|multibuffer, cx| {
140            let buffer = multibuffer.as_singleton().unwrap();
141            buffer.update(cx, update)
142        })
143    }
144
145    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
146        self.buffer(|buffer, _| buffer.snapshot())
147    }
148
149    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
150        self.assertion_cx.add_context(context)
151    }
152
153    pub fn assertion_context(&self) -> String {
154        self.assertion_cx.context()
155    }
156
157    pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
158        let keystroke_under_test_handle =
159            self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
160        let keystroke = Keystroke::parse(keystroke_text).unwrap();
161
162        self.cx.dispatch_keystroke(self.window, keystroke);
163
164        keystroke_under_test_handle
165    }
166
167    pub fn simulate_keystrokes<const COUNT: usize>(
168        &mut self,
169        keystroke_texts: [&str; COUNT],
170    ) -> ContextHandle {
171        let keystrokes_under_test_handle =
172            self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
173        for keystroke_text in keystroke_texts.into_iter() {
174            self.simulate_keystroke(keystroke_text);
175        }
176        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
177        // before returning.
178        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
179        // quickly races with async actions.
180        self.cx.background_executor.run_until_parked();
181
182        keystrokes_under_test_handle
183    }
184
185    pub fn run_until_parked(&mut self) {
186        self.cx.background_executor.run_until_parked();
187    }
188
189    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
190        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
191        assert_eq!(self.buffer_text(), unmarked_text);
192        ranges
193    }
194
195    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
196        let ranges = self.ranges(marked_text);
197        let snapshot = self
198            .editor
199            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
200        ranges[0].start.to_display_point(&snapshot)
201    }
202
203    pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
204        let display_point = self.display_point(marked_text);
205        self.pixel_position_for(display_point)
206    }
207
208    pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
209        self.update_editor(|editor, cx| {
210            let newest_point = editor.selections.newest_display(cx).head();
211            let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
212            let line_height = editor
213                .style()
214                .unwrap()
215                .text
216                .line_height_in_pixels(cx.rem_size());
217            let snapshot = editor.snapshot(cx);
218            let details = editor.text_layout_details(cx);
219
220            let y = pixel_position.y
221                + line_height * (display_point.row() as f32 - newest_point.row() as f32);
222            let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
223                - snapshot.x_for_display_point(newest_point, &details);
224            Point::new(x, y)
225        })
226    }
227
228    // Returns anchors for the current buffer using `«` and `»`
229    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
230        let ranges = self.ranges(marked_text);
231        let snapshot = self.buffer_snapshot();
232        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
233    }
234
235    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
236        let diff_base = diff_base.map(String::from);
237        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
238    }
239
240    /// Change the editor's text and selections using a string containing
241    /// embedded range markers that represent the ranges and directions of
242    /// each selection.
243    ///
244    /// Returns a context handle so that assertion failures can print what
245    /// editor state was needed to cause the failure.
246    ///
247    /// See the `util::test::marked_text_ranges` function for more information.
248    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
249        let state_context = self.add_assertion_context(format!(
250            "Initial Editor State: \"{}\"",
251            marked_text.escape_debug()
252        ));
253        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
254        self.editor.update(&mut self.cx, |editor, cx| {
255            editor.set_text(unmarked_text, cx);
256            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
257                s.select_ranges(selection_ranges)
258            })
259        });
260        state_context
261    }
262
263    /// Only change the editor's selections
264    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
265        let state_context = self.add_assertion_context(format!(
266            "Initial Editor State: \"{}\"",
267            marked_text.escape_debug()
268        ));
269        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
270        self.editor.update(&mut self.cx, |editor, cx| {
271            assert_eq!(editor.text(cx), unmarked_text);
272            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
273                s.select_ranges(selection_ranges)
274            })
275        });
276        state_context
277    }
278
279    /// Make an assertion about the editor's text and the ranges and directions
280    /// of its selections using a string containing embedded range markers.
281    ///
282    /// See the `util::test::marked_text_ranges` function for more information.
283    #[track_caller]
284    pub fn assert_editor_state(&mut self, marked_text: &str) {
285        let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
286        let buffer_text = self.buffer_text();
287
288        if buffer_text != unmarked_text {
289            panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}\nRaw unmarked text\n{unmarked_text}");
290        }
291
292        self.assert_selections(expected_selections, marked_text.to_string())
293    }
294
295    pub fn editor_state(&mut self) -> String {
296        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
297    }
298
299    #[track_caller]
300    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
301        let expected_ranges = self.ranges(marked_text);
302        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
303            let snapshot = editor.snapshot(cx);
304            editor
305                .background_highlights
306                .get(&TypeId::of::<Tag>())
307                .map(|h| h.1.clone())
308                .unwrap_or_default()
309                .into_iter()
310                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
311                .collect()
312        });
313        assert_set_eq!(actual_ranges, expected_ranges);
314    }
315
316    #[track_caller]
317    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
318        let expected_ranges = self.ranges(marked_text);
319        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
320        let actual_ranges: Vec<Range<usize>> = snapshot
321            .text_highlight_ranges::<Tag>()
322            .map(|ranges| ranges.as_ref().clone().1)
323            .unwrap_or_default()
324            .into_iter()
325            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
326            .collect();
327        assert_set_eq!(actual_ranges, expected_ranges);
328    }
329
330    #[track_caller]
331    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
332        let expected_marked_text =
333            generate_marked_text(&self.buffer_text(), &expected_selections, true);
334        self.assert_selections(expected_selections, expected_marked_text)
335    }
336
337    #[track_caller]
338    fn editor_selections(&mut self) -> Vec<Range<usize>> {
339        self.editor
340            .update(&mut self.cx, |editor, cx| {
341                editor.selections.all::<usize>(cx)
342            })
343            .into_iter()
344            .map(|s| {
345                if s.reversed {
346                    s.end..s.start
347                } else {
348                    s.start..s.end
349                }
350            })
351            .collect::<Vec<_>>()
352    }
353
354    #[track_caller]
355    fn assert_selections(
356        &mut self,
357        expected_selections: Vec<Range<usize>>,
358        expected_marked_text: String,
359    ) {
360        let actual_selections = self.editor_selections();
361        let actual_marked_text =
362            generate_marked_text(&self.buffer_text(), &actual_selections, true);
363        if expected_selections != actual_selections {
364            panic!(
365                indoc! {"
366
367                {}Editor has unexpected selections.
368
369                Expected selections:
370                {}
371
372                Actual selections:
373                {}
374            "},
375                self.assertion_context(),
376                expected_marked_text,
377                actual_marked_text,
378            );
379        }
380    }
381}
382
383impl Deref for EditorTestContext {
384    type Target = gpui::VisualTestContext;
385
386    fn deref(&self) -> &Self::Target {
387        &self.cx
388    }
389}
390
391impl DerefMut for EditorTestContext {
392    fn deref_mut(&mut self) -> &mut Self::Target {
393        &mut self.cx
394    }
395}
396
397/// Tracks string context to be printed when assertions fail.
398/// Often this is done by storing a context string in the manager and returning the handle.
399#[derive(Clone)]
400pub struct AssertionContextManager {
401    id: Arc<AtomicUsize>,
402    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
403}
404
405impl AssertionContextManager {
406    pub fn new() -> Self {
407        Self {
408            id: Arc::new(AtomicUsize::new(0)),
409            contexts: Arc::new(RwLock::new(BTreeMap::new())),
410        }
411    }
412
413    pub fn add_context(&self, context: String) -> ContextHandle {
414        let id = self.id.fetch_add(1, Ordering::Relaxed);
415        let mut contexts = self.contexts.write();
416        contexts.insert(id, context);
417        ContextHandle {
418            id,
419            manager: self.clone(),
420        }
421    }
422
423    pub fn context(&self) -> String {
424        let contexts = self.contexts.read();
425        format!("\n{}\n", contexts.values().join("\n"))
426    }
427}
428
429/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
430/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
431/// the state that was set initially for the failure can be printed in the error message
432pub struct ContextHandle {
433    id: usize,
434    manager: AssertionContextManager,
435}
436
437impl Drop for ContextHandle {
438    fn drop(&mut self) {
439        let mut contexts = self.manager.contexts.write();
440        contexts.remove(&self.id);
441    }
442}