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};
 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 update_buffer<F, T>(&mut self, update: F) -> T
124    where
125        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
126    {
127        self.update_multibuffer(|multibuffer, cx| {
128            let buffer = multibuffer.as_singleton().unwrap();
129            buffer.update(cx, update)
130        })
131    }
132
133    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
134        self.buffer(|buffer, _| buffer.snapshot())
135    }
136
137    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
138        self.assertion_cx.add_context(context)
139    }
140
141    pub fn assertion_context(&self) -> String {
142        self.assertion_cx.context()
143    }
144
145    pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
146        let keystroke_under_test_handle =
147            self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
148        let keystroke = Keystroke::parse(keystroke_text).unwrap();
149
150        self.cx.dispatch_keystroke(self.window, keystroke, false);
151
152        keystroke_under_test_handle
153    }
154
155    pub fn simulate_keystrokes<const COUNT: usize>(
156        &mut self,
157        keystroke_texts: [&str; COUNT],
158    ) -> ContextHandle {
159        let keystrokes_under_test_handle =
160            self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
161        for keystroke_text in keystroke_texts.into_iter() {
162            self.simulate_keystroke(keystroke_text);
163        }
164        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
165        // before returning.
166        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
167        // quickly races with async actions.
168        self.cx.background_executor.run_until_parked();
169
170        keystrokes_under_test_handle
171    }
172
173    pub fn run_until_parked(&mut self) {
174        self.cx.background_executor.run_until_parked();
175    }
176
177    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
178        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
179        assert_eq!(self.buffer_text(), unmarked_text);
180        ranges
181    }
182
183    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
184        let ranges = self.ranges(marked_text);
185        let snapshot = self
186            .editor
187            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
188        ranges[0].start.to_display_point(&snapshot)
189    }
190
191    pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
192        let display_point = self.display_point(marked_text);
193        self.pixel_position_for(display_point)
194    }
195
196    pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
197        self.update_editor(|editor, cx| {
198            let newest_point = editor.selections.newest_display(cx).head();
199            let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
200            let line_height = editor
201                .style()
202                .unwrap()
203                .text
204                .line_height_in_pixels(cx.rem_size());
205            let snapshot = editor.snapshot(cx);
206            let details = editor.text_layout_details(cx);
207
208            let y = pixel_position.y
209                + line_height * (display_point.row() as f32 - newest_point.row() as f32);
210            let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
211                - snapshot.x_for_display_point(newest_point, &details);
212            Point::new(x, y)
213        })
214    }
215
216    // Returns anchors for the current buffer using `«` and `»`
217    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
218        let ranges = self.ranges(marked_text);
219        let snapshot = self.buffer_snapshot();
220        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
221    }
222
223    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
224        let diff_base = diff_base.map(String::from);
225        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
226    }
227
228    /// Change the editor's text and selections using a string containing
229    /// embedded range markers that represent the ranges and directions of
230    /// each selection.
231    ///
232    /// Returns a context handle so that assertion failures can print what
233    /// editor state was needed to cause the failure.
234    ///
235    /// See the `util::test::marked_text_ranges` function for more information.
236    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
237        let state_context = self.add_assertion_context(format!(
238            "Initial Editor State: \"{}\"",
239            marked_text.escape_debug().to_string()
240        ));
241        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
242        self.editor.update(&mut self.cx, |editor, cx| {
243            editor.set_text(unmarked_text, cx);
244            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
245                s.select_ranges(selection_ranges)
246            })
247        });
248        state_context
249    }
250
251    /// Only change the editor's selections
252    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
253        let state_context = self.add_assertion_context(format!(
254            "Initial Editor State: \"{}\"",
255            marked_text.escape_debug().to_string()
256        ));
257        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
258        self.editor.update(&mut self.cx, |editor, cx| {
259            assert_eq!(editor.text(cx), unmarked_text);
260            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
261                s.select_ranges(selection_ranges)
262            })
263        });
264        state_context
265    }
266
267    /// Make an assertion about the editor's text and the ranges and directions
268    /// of its selections using a string containing embedded range markers.
269    ///
270    /// See the `util::test::marked_text_ranges` function for more information.
271    #[track_caller]
272    pub fn assert_editor_state(&mut self, marked_text: &str) {
273        let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
274        let buffer_text = self.buffer_text();
275
276        if buffer_text != unmarked_text {
277            panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
278        }
279
280        self.assert_selections(expected_selections, marked_text.to_string())
281    }
282
283    pub fn editor_state(&mut self) -> String {
284        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
285    }
286
287    #[track_caller]
288    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
289        let expected_ranges = self.ranges(marked_text);
290        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
291            let snapshot = editor.snapshot(cx);
292            editor
293                .background_highlights
294                .get(&TypeId::of::<Tag>())
295                .map(|h| h.1.clone())
296                .unwrap_or_default()
297                .into_iter()
298                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
299                .collect()
300        });
301        assert_set_eq!(actual_ranges, expected_ranges);
302    }
303
304    #[track_caller]
305    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
306        let expected_ranges = self.ranges(marked_text);
307        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
308        let actual_ranges: Vec<Range<usize>> = snapshot
309            .text_highlight_ranges::<Tag>()
310            .map(|ranges| ranges.as_ref().clone().1)
311            .unwrap_or_default()
312            .into_iter()
313            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
314            .collect();
315        assert_set_eq!(actual_ranges, expected_ranges);
316    }
317
318    #[track_caller]
319    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
320        let expected_marked_text =
321            generate_marked_text(&self.buffer_text(), &expected_selections, true);
322        self.assert_selections(expected_selections, expected_marked_text)
323    }
324
325    #[track_caller]
326    fn editor_selections(&mut self) -> Vec<Range<usize>> {
327        self.editor
328            .update(&mut self.cx, |editor, cx| {
329                editor.selections.all::<usize>(cx)
330            })
331            .into_iter()
332            .map(|s| {
333                if s.reversed {
334                    s.end..s.start
335                } else {
336                    s.start..s.end
337                }
338            })
339            .collect::<Vec<_>>()
340    }
341
342    #[track_caller]
343    fn assert_selections(
344        &mut self,
345        expected_selections: Vec<Range<usize>>,
346        expected_marked_text: String,
347    ) {
348        let actual_selections = self.editor_selections();
349        let actual_marked_text =
350            generate_marked_text(&self.buffer_text(), &actual_selections, true);
351        if expected_selections != actual_selections {
352            panic!(
353                indoc! {"
354
355                {}Editor has unexpected selections.
356
357                Expected selections:
358                {}
359
360                Actual selections:
361                {}
362            "},
363                self.assertion_context(),
364                expected_marked_text,
365                actual_marked_text,
366            );
367        }
368    }
369}
370
371impl Deref for EditorTestContext {
372    type Target = gpui::VisualTestContext;
373
374    fn deref(&self) -> &Self::Target {
375        &self.cx
376    }
377}
378
379impl DerefMut for EditorTestContext {
380    fn deref_mut(&mut self) -> &mut Self::Target {
381        &mut self.cx
382    }
383}
384
385/// Tracks string context to be printed when assertions fail.
386/// Often this is done by storing a context string in the manager and returning the handle.
387#[derive(Clone)]
388pub struct AssertionContextManager {
389    id: Arc<AtomicUsize>,
390    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
391}
392
393impl AssertionContextManager {
394    pub fn new() -> Self {
395        Self {
396            id: Arc::new(AtomicUsize::new(0)),
397            contexts: Arc::new(RwLock::new(BTreeMap::new())),
398        }
399    }
400
401    pub fn add_context(&self, context: String) -> ContextHandle {
402        let id = self.id.fetch_add(1, Ordering::Relaxed);
403        let mut contexts = self.contexts.write();
404        contexts.insert(id, context);
405        ContextHandle {
406            id,
407            manager: self.clone(),
408        }
409    }
410
411    pub fn context(&self) -> String {
412        let contexts = self.contexts.read();
413        format!("\n{}\n", contexts.values().join("\n"))
414    }
415}
416
417/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
418/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
419/// the state that was set initially for the failure can be printed in the error message
420pub struct ContextHandle {
421    id: usize,
422    manager: AssertionContextManager,
423}
424
425impl Drop for ContextHandle {
426    fn drop(&mut self) {
427        let mut contexts = self.manager.contexts.write();
428        contexts.remove(&self.id);
429    }
430}