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