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