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, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
  8    VisualTestContext, WindowHandle,
  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<'a> {
 31    pub cx: gpui::VisualTestContext<'a>,
 32    pub window: AnyWindowHandle,
 33    pub editor: View<Editor>,
 34    pub assertion_cx: AssertionContextManager,
 35}
 36
 37impl<'a> EditorTestContext<'a> {
 38    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
 39        let fs = FakeFs::new(cx.executor());
 40        // fs.insert_file("/file", "".to_owned()).await;
 41        fs.insert_tree(
 42            "/root",
 43            gpui::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 ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
174        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
175        assert_eq!(self.buffer_text(), unmarked_text);
176        ranges
177    }
178
179    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
180        let ranges = self.ranges(marked_text);
181        let snapshot = self
182            .editor
183            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
184        ranges[0].start.to_display_point(&snapshot)
185    }
186
187    // Returns anchors for the current buffer using `«` and `»`
188    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
189        let ranges = self.ranges(marked_text);
190        let snapshot = self.buffer_snapshot();
191        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
192    }
193
194    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
195        let diff_base = diff_base.map(String::from);
196        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
197    }
198
199    /// Change the editor's text and selections using a string containing
200    /// embedded range markers that represent the ranges and directions of
201    /// each selection.
202    ///
203    /// Returns a context handle so that assertion failures can print what
204    /// editor state was needed to cause the failure.
205    ///
206    /// See the `util::test::marked_text_ranges` function for more information.
207    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
208        let state_context = self.add_assertion_context(format!(
209            "Initial Editor State: \"{}\"",
210            marked_text.escape_debug().to_string()
211        ));
212        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
213        self.editor.update(&mut self.cx, |editor, cx| {
214            editor.set_text(unmarked_text, cx);
215            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
216                s.select_ranges(selection_ranges)
217            })
218        });
219        state_context
220    }
221
222    /// Only change the editor's selections
223    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
224        let state_context = self.add_assertion_context(format!(
225            "Initial Editor State: \"{}\"",
226            marked_text.escape_debug().to_string()
227        ));
228        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
229        self.editor.update(&mut self.cx, |editor, cx| {
230            assert_eq!(editor.text(cx), unmarked_text);
231            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232                s.select_ranges(selection_ranges)
233            })
234        });
235        state_context
236    }
237
238    /// Make an assertion about the editor's text and the ranges and directions
239    /// of its selections using a string containing embedded range markers.
240    ///
241    /// See the `util::test::marked_text_ranges` function for more information.
242    #[track_caller]
243    pub fn assert_editor_state(&mut self, marked_text: &str) {
244        let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
245        let buffer_text = self.buffer_text();
246
247        if buffer_text != unmarked_text {
248            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}");
249        }
250
251        self.assert_selections(expected_selections, marked_text.to_string())
252    }
253
254    pub fn editor_state(&mut self) -> String {
255        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
256    }
257
258    #[track_caller]
259    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
260        let expected_ranges = self.ranges(marked_text);
261        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
262            let snapshot = editor.snapshot(cx);
263            editor
264                .background_highlights
265                .get(&TypeId::of::<Tag>())
266                .map(|h| h.1.clone())
267                .unwrap_or_default()
268                .into_iter()
269                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
270                .collect()
271        });
272        assert_set_eq!(actual_ranges, expected_ranges);
273    }
274
275    #[track_caller]
276    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
277        let expected_ranges = self.ranges(marked_text);
278        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
279        let actual_ranges: Vec<Range<usize>> = snapshot
280            .text_highlight_ranges::<Tag>()
281            .map(|ranges| ranges.as_ref().clone().1)
282            .unwrap_or_default()
283            .into_iter()
284            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
285            .collect();
286        assert_set_eq!(actual_ranges, expected_ranges);
287    }
288
289    #[track_caller]
290    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
291        let expected_marked_text =
292            generate_marked_text(&self.buffer_text(), &expected_selections, true);
293        self.assert_selections(expected_selections, expected_marked_text)
294    }
295
296    #[track_caller]
297    fn editor_selections(&mut self) -> Vec<Range<usize>> {
298        self.editor
299            .update(&mut self.cx, |editor, cx| {
300                editor.selections.all::<usize>(cx)
301            })
302            .into_iter()
303            .map(|s| {
304                if s.reversed {
305                    s.end..s.start
306                } else {
307                    s.start..s.end
308                }
309            })
310            .collect::<Vec<_>>()
311    }
312
313    #[track_caller]
314    fn assert_selections(
315        &mut self,
316        expected_selections: Vec<Range<usize>>,
317        expected_marked_text: String,
318    ) {
319        let actual_selections = self.editor_selections();
320        let actual_marked_text =
321            generate_marked_text(&self.buffer_text(), &actual_selections, true);
322        if expected_selections != actual_selections {
323            panic!(
324                indoc! {"
325
326                {}Editor has unexpected selections.
327
328                Expected selections:
329                {}
330
331                Actual selections:
332                {}
333            "},
334                self.assertion_context(),
335                expected_marked_text,
336                actual_marked_text,
337            );
338        }
339    }
340}
341
342impl<'a> Deref for EditorTestContext<'a> {
343    type Target = gpui::TestAppContext;
344
345    fn deref(&self) -> &Self::Target {
346        &self.cx
347    }
348}
349
350impl<'a> DerefMut for EditorTestContext<'a> {
351    fn deref_mut(&mut self) -> &mut Self::Target {
352        &mut self.cx
353    }
354}
355
356/// Tracks string context to be printed when assertions fail.
357/// Often this is done by storing a context string in the manager and returning the handle.
358#[derive(Clone)]
359pub struct AssertionContextManager {
360    id: Arc<AtomicUsize>,
361    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
362}
363
364impl AssertionContextManager {
365    pub fn new() -> Self {
366        Self {
367            id: Arc::new(AtomicUsize::new(0)),
368            contexts: Arc::new(RwLock::new(BTreeMap::new())),
369        }
370    }
371
372    pub fn add_context(&self, context: String) -> ContextHandle {
373        let id = self.id.fetch_add(1, Ordering::Relaxed);
374        let mut contexts = self.contexts.write();
375        contexts.insert(id, context);
376        ContextHandle {
377            id,
378            manager: self.clone(),
379        }
380    }
381
382    pub fn context(&self) -> String {
383        let contexts = self.contexts.read();
384        format!("\n{}\n", contexts.values().join("\n"))
385    }
386}
387
388/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
389/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
390/// the state that was set initially for the failure can be printed in the error message
391pub struct ContextHandle {
392    id: usize,
393    manager: AssertionContextManager,
394}
395
396impl Drop for ContextHandle {
397    fn drop(&mut self) {
398        let mut contexts = self.manager.contexts.write();
399        contexts.remove(&self.id);
400    }
401}