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