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}