editor_test_context.rs

  1use crate::{
  2    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint,
  3    Editor, MultiBuffer, RowExt,
  4};
  5use collections::BTreeMap;
  6use futures::Future;
  7use git::diff::DiffHunkStatus;
  8use gpui::{
  9    AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
 10    VisualTestContext, WindowHandle,
 11};
 12use itertools::Itertools;
 13use language::{Buffer, BufferSnapshot, LanguageRegistry};
 14use multi_buffer::{ExcerptRange, ToPoint};
 15use parking_lot::RwLock;
 16use project::{FakeFs, Project};
 17use std::{
 18    any::TypeId,
 19    ops::{Deref, DerefMut, Range},
 20    sync::{
 21        atomic::{AtomicUsize, Ordering},
 22        Arc,
 23    },
 24};
 25
 26use ui::Context;
 27use util::{
 28    assert_set_eq,
 29    test::{generate_marked_text, marked_text_ranges},
 30};
 31
 32use super::{build_editor, build_editor_with_project};
 33
 34pub struct EditorTestContext {
 35    pub cx: gpui::VisualTestContext,
 36    pub window: AnyWindowHandle,
 37    pub editor: View<Editor>,
 38    pub assertion_cx: AssertionContextManager,
 39}
 40
 41impl EditorTestContext {
 42    pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
 43        let fs = FakeFs::new(cx.executor());
 44        // fs.insert_file("/file", "".to_owned()).await;
 45        fs.insert_tree(
 46            "/root",
 47            serde_json::json!({
 48                "file": "",
 49            }),
 50        )
 51        .await;
 52        let project = Project::test(fs, ["/root".as_ref()], cx).await;
 53        let buffer = project
 54            .update(cx, |project, cx| {
 55                project.open_local_buffer("/root/file", cx)
 56            })
 57            .await
 58            .unwrap();
 59        let editor = cx.add_window(|cx| {
 60            let editor =
 61                build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
 62            editor.focus(cx);
 63            editor
 64        });
 65        let editor_view = editor.root_view(cx).unwrap();
 66        Self {
 67            cx: VisualTestContext::from_window(*editor.deref(), cx),
 68            window: editor.into(),
 69            editor: editor_view,
 70            assertion_cx: AssertionContextManager::new(),
 71        }
 72    }
 73
 74    pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
 75        let editor_view = editor.root_view(cx).unwrap();
 76        Self {
 77            cx: VisualTestContext::from_window(*editor.deref(), cx),
 78            window: editor.into(),
 79            editor: editor_view,
 80            assertion_cx: AssertionContextManager::new(),
 81        }
 82    }
 83
 84    pub fn new_multibuffer<const COUNT: usize>(
 85        cx: &mut gpui::TestAppContext,
 86        excerpts: [&str; COUNT],
 87    ) -> EditorTestContext {
 88        let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
 89        let buffer = cx.new_model(|cx| {
 90            for excerpt in excerpts.into_iter() {
 91                let (text, ranges) = marked_text_ranges(excerpt, false);
 92                let buffer = cx.new_model(|cx| Buffer::local(text, cx));
 93                multibuffer.push_excerpts(
 94                    buffer,
 95                    ranges.into_iter().map(|range| ExcerptRange {
 96                        context: range,
 97                        primary: None,
 98                    }),
 99                    cx,
100                );
101            }
102            multibuffer
103        });
104
105        let editor = cx.add_window(|cx| {
106            let editor = build_editor(buffer, cx);
107            editor.focus(cx);
108            editor
109        });
110
111        let editor_view = editor.root_view(cx).unwrap();
112        Self {
113            cx: VisualTestContext::from_window(*editor.deref(), cx),
114            window: editor.into(),
115            editor: editor_view,
116            assertion_cx: AssertionContextManager::new(),
117        }
118    }
119
120    pub fn condition(
121        &self,
122        predicate: impl FnMut(&Editor, &AppContext) -> bool,
123    ) -> impl Future<Output = ()> {
124        self.editor
125            .condition::<crate::EditorEvent>(&self.cx, predicate)
126    }
127
128    #[track_caller]
129    pub fn editor<F, T>(&mut self, read: F) -> T
130    where
131        F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
132    {
133        self.editor.update(&mut self.cx, |this, cx| read(this, cx))
134    }
135
136    #[track_caller]
137    pub fn update_editor<F, T>(&mut self, update: F) -> T
138    where
139        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
140    {
141        self.editor.update(&mut self.cx, update)
142    }
143
144    pub fn multibuffer<F, T>(&mut self, read: F) -> T
145    where
146        F: FnOnce(&MultiBuffer, &AppContext) -> T,
147    {
148        self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
149    }
150
151    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
152    where
153        F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
154    {
155        self.update_editor(|editor, cx| editor.buffer().update(cx, update))
156    }
157
158    pub fn buffer_text(&mut self) -> String {
159        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
160    }
161
162    pub fn display_text(&mut self) -> String {
163        self.update_editor(|editor, cx| editor.display_text(cx))
164    }
165
166    pub fn buffer<F, T>(&mut self, read: F) -> T
167    where
168        F: FnOnce(&Buffer, &AppContext) -> T,
169    {
170        self.multibuffer(|multibuffer, cx| {
171            let buffer = multibuffer.as_singleton().unwrap().read(cx);
172            read(buffer, cx)
173        })
174    }
175
176    pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
177        self.editor(|editor, cx| {
178            editor
179                .project
180                .as_ref()
181                .unwrap()
182                .read(cx)
183                .languages()
184                .clone()
185        })
186    }
187
188    pub fn update_buffer<F, T>(&mut self, update: F) -> T
189    where
190        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
191    {
192        self.update_multibuffer(|multibuffer, cx| {
193            let buffer = multibuffer.as_singleton().unwrap();
194            buffer.update(cx, update)
195        })
196    }
197
198    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
199        self.buffer(|buffer, _| buffer.snapshot())
200    }
201
202    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
203        self.assertion_cx.add_context(context)
204    }
205
206    pub fn assertion_context(&self) -> String {
207        self.assertion_cx.context()
208    }
209
210    // unlike cx.simulate_keystrokes(), this does not run_until_parked
211    // so you can use it to test detailed timing
212    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
213        let keystroke = Keystroke::parse(keystroke_text).unwrap();
214        self.cx.dispatch_keystroke(self.window, keystroke);
215    }
216
217    pub fn run_until_parked(&mut self) {
218        self.cx.background_executor.run_until_parked();
219    }
220
221    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
222        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
223        assert_eq!(self.buffer_text(), unmarked_text);
224        ranges
225    }
226
227    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
228        let ranges = self.ranges(marked_text);
229        let snapshot = self
230            .editor
231            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
232        ranges[0].start.to_display_point(&snapshot)
233    }
234
235    pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
236        let display_point = self.display_point(marked_text);
237        self.pixel_position_for(display_point)
238    }
239
240    pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
241        self.update_editor(|editor, cx| {
242            let newest_point = editor.selections.newest_display(cx).head();
243            let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
244            let line_height = editor
245                .style()
246                .unwrap()
247                .text
248                .line_height_in_pixels(cx.rem_size());
249            let snapshot = editor.snapshot(cx);
250            let details = editor.text_layout_details(cx);
251
252            let y = pixel_position.y
253                + line_height * (display_point.row().as_f32() - newest_point.row().as_f32());
254            let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
255                - snapshot.x_for_display_point(newest_point, &details);
256            Point::new(x, y)
257        })
258    }
259
260    // Returns anchors for the current buffer using `«` and `»`
261    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
262        let ranges = self.ranges(marked_text);
263        let snapshot = self.buffer_snapshot();
264        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
265    }
266
267    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
268        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base.map(ToOwned::to_owned), cx));
269    }
270
271    /// Change the editor's text and selections using a string containing
272    /// embedded range markers that represent the ranges and directions of
273    /// each selection.
274    ///
275    /// Returns a context handle so that assertion failures can print what
276    /// editor state was needed to cause the failure.
277    ///
278    /// See the `util::test::marked_text_ranges` function for more information.
279    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
280        let state_context = self.add_assertion_context(format!(
281            "Initial Editor State: \"{}\"",
282            marked_text.escape_debug()
283        ));
284        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
285        self.editor.update(&mut self.cx, |editor, cx| {
286            editor.set_text(unmarked_text, cx);
287            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
288                s.select_ranges(selection_ranges)
289            })
290        });
291        state_context
292    }
293
294    /// Only change the editor's selections
295    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
296        let state_context = self.add_assertion_context(format!(
297            "Initial Editor State: \"{}\"",
298            marked_text.escape_debug()
299        ));
300        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
301        self.editor.update(&mut self.cx, |editor, cx| {
302            assert_eq!(editor.text(cx), unmarked_text);
303            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
304                s.select_ranges(selection_ranges)
305            })
306        });
307        state_context
308    }
309
310    #[track_caller]
311    pub fn assert_diff_hunks(&mut self, expected_diff: String) {
312        // Normalize the expected diff. If it has no diff markers, then insert blank markers
313        // before each line. Strip any whitespace-only lines.
314        let has_diff_markers = expected_diff
315            .lines()
316            .any(|line| line.starts_with("+") || line.starts_with("-"));
317        let expected_diff_text = expected_diff
318            .split('\n')
319            .map(|line| {
320                let trimmed = line.trim();
321                if trimmed.is_empty() {
322                    String::new()
323                } else if has_diff_markers {
324                    line.to_string()
325                } else {
326                    format!("  {line}")
327                }
328            })
329            .join("\n");
330
331        // Read the actual diff from the editor's row highlights and block
332        // decorations.
333        let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
334            let snapshot = editor.snapshot(cx);
335            let text = editor.text(cx);
336            let insertions = editor
337                .highlighted_rows::<DiffRowHighlight>()
338                .map(|(range, _)| {
339                    let start = range.start.to_point(&snapshot.buffer_snapshot);
340                    let end = range.end.to_point(&snapshot.buffer_snapshot);
341                    start.row..end.row
342                })
343                .collect::<Vec<_>>();
344            let deletions = editor
345                .expanded_hunks
346                .hunks
347                .iter()
348                .filter_map(|hunk| {
349                    if hunk.blocks.is_empty() {
350                        return None;
351                    }
352                    let row = hunk
353                        .hunk_range
354                        .start
355                        .to_point(&snapshot.buffer_snapshot)
356                        .row;
357                    let (_, buffer, _) = editor
358                        .buffer()
359                        .read(cx)
360                        .excerpt_containing(hunk.hunk_range.start, cx)
361                        .expect("no excerpt for expanded buffer's hunk start");
362                    let deleted_text = buffer
363                        .read(cx)
364                        .diff_base()
365                        .expect("should have a diff base for expanded hunk")
366                        .slice(hunk.diff_base_byte_range.clone())
367                        .to_string();
368                    if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
369                        Some((row, deleted_text))
370                    } else {
371                        None
372                    }
373                })
374                .collect::<Vec<_>>();
375            format_diff(text, deletions, insertions)
376        });
377
378        pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
379    }
380
381    /// Make an assertion about the editor's text and the ranges and directions
382    /// of its selections using a string containing embedded range markers.
383    ///
384    /// See the `util::test::marked_text_ranges` function for more information.
385    #[track_caller]
386    pub fn assert_editor_state(&mut self, marked_text: &str) {
387        let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
388        pretty_assertions::assert_eq!(self.buffer_text(), expected_text, "unexpected buffer text");
389        self.assert_selections(expected_selections, marked_text.to_string())
390    }
391
392    pub fn editor_state(&mut self) -> String {
393        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
394    }
395
396    #[track_caller]
397    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
398        let expected_ranges = self.ranges(marked_text);
399        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
400            let snapshot = editor.snapshot(cx);
401            editor
402                .background_highlights
403                .get(&TypeId::of::<Tag>())
404                .map(|h| h.1.clone())
405                .unwrap_or_default()
406                .iter()
407                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
408                .collect()
409        });
410        assert_set_eq!(actual_ranges, expected_ranges);
411    }
412
413    #[track_caller]
414    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
415        let expected_ranges = self.ranges(marked_text);
416        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
417        let actual_ranges: Vec<Range<usize>> = snapshot
418            .text_highlight_ranges::<Tag>()
419            .map(|ranges| ranges.as_ref().clone().1)
420            .unwrap_or_default()
421            .into_iter()
422            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
423            .collect();
424        assert_set_eq!(actual_ranges, expected_ranges);
425    }
426
427    #[track_caller]
428    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
429        let expected_marked_text =
430            generate_marked_text(&self.buffer_text(), &expected_selections, true);
431        self.assert_selections(expected_selections, expected_marked_text)
432    }
433
434    #[track_caller]
435    fn editor_selections(&mut self) -> Vec<Range<usize>> {
436        self.editor
437            .update(&mut self.cx, |editor, cx| {
438                editor.selections.all::<usize>(cx)
439            })
440            .into_iter()
441            .map(|s| {
442                if s.reversed {
443                    s.end..s.start
444                } else {
445                    s.start..s.end
446                }
447            })
448            .collect::<Vec<_>>()
449    }
450
451    #[track_caller]
452    fn assert_selections(
453        &mut self,
454        expected_selections: Vec<Range<usize>>,
455        expected_marked_text: String,
456    ) {
457        let actual_selections = self.editor_selections();
458        let actual_marked_text =
459            generate_marked_text(&self.buffer_text(), &actual_selections, true);
460        if expected_selections != actual_selections {
461            pretty_assertions::assert_eq!(
462                actual_marked_text,
463                expected_marked_text,
464                "{}Editor has unexpected selections",
465                self.assertion_context(),
466            );
467        }
468    }
469}
470
471fn format_diff(
472    text: String,
473    actual_deletions: Vec<(u32, String)>,
474    actual_insertions: Vec<Range<u32>>,
475) -> String {
476    let mut diff = String::new();
477    for (row, line) in text.split('\n').enumerate() {
478        let row = row as u32;
479        if row > 0 {
480            diff.push('\n');
481        }
482        if let Some(text) = actual_deletions
483            .iter()
484            .find_map(|(deletion_row, deleted_text)| {
485                if *deletion_row == row {
486                    Some(deleted_text)
487                } else {
488                    None
489                }
490            })
491        {
492            for line in text.lines() {
493                diff.push('-');
494                if !line.is_empty() {
495                    diff.push(' ');
496                    diff.push_str(line);
497                }
498                diff.push('\n');
499            }
500        }
501        let marker = if actual_insertions.iter().any(|range| range.contains(&row)) {
502            "+ "
503        } else {
504            "  "
505        };
506        diff.push_str(format!("{marker}{line}").trim_end());
507    }
508    diff
509}
510
511impl Deref for EditorTestContext {
512    type Target = gpui::VisualTestContext;
513
514    fn deref(&self) -> &Self::Target {
515        &self.cx
516    }
517}
518
519impl DerefMut for EditorTestContext {
520    fn deref_mut(&mut self) -> &mut Self::Target {
521        &mut self.cx
522    }
523}
524
525/// Tracks string context to be printed when assertions fail.
526/// Often this is done by storing a context string in the manager and returning the handle.
527#[derive(Clone)]
528pub struct AssertionContextManager {
529    id: Arc<AtomicUsize>,
530    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
531}
532
533impl Default for AssertionContextManager {
534    fn default() -> Self {
535        Self::new()
536    }
537}
538
539impl AssertionContextManager {
540    pub fn new() -> Self {
541        Self {
542            id: Arc::new(AtomicUsize::new(0)),
543            contexts: Arc::new(RwLock::new(BTreeMap::new())),
544        }
545    }
546
547    pub fn add_context(&self, context: String) -> ContextHandle {
548        let id = self.id.fetch_add(1, Ordering::Relaxed);
549        let mut contexts = self.contexts.write();
550        contexts.insert(id, context);
551        ContextHandle {
552            id,
553            manager: self.clone(),
554        }
555    }
556
557    pub fn context(&self) -> String {
558        let contexts = self.contexts.read();
559        format!("\n{}\n", contexts.values().join("\n"))
560    }
561}
562
563/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
564/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
565/// the state that was set initially for the failure can be printed in the error message
566pub struct ContextHandle {
567    id: usize,
568    manager: AssertionContextManager,
569}
570
571impl Drop for ContextHandle {
572    fn drop(&mut self) {
573        let mut contexts = self.manager.contexts.write();
574        contexts.remove(&self.id);
575    }
576}