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