1use super::*;
2use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
3use gpui::{App, TestAppContext};
4use indoc::indoc;
5use language::{Buffer, Rope};
6use parking_lot::RwLock;
7use rand::prelude::*;
8use settings::SettingsStore;
9use std::env;
10use util::test::sample_text;
11
12#[ctor::ctor]
13fn init_logger() {
14 if std::env::var("RUST_LOG").is_ok() {
15 env_logger::init();
16 }
17}
18
19#[gpui::test]
20fn test_empty_singleton(cx: &mut App) {
21 let buffer = cx.new(|cx| Buffer::local("", cx));
22 let buffer_id = buffer.read(cx).remote_id();
23 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
24 let snapshot = multibuffer.read(cx).snapshot(cx);
25 assert_eq!(snapshot.text(), "");
26 assert_eq!(
27 snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
28 [RowInfo {
29 buffer_id: Some(buffer_id),
30 buffer_row: Some(0),
31 multibuffer_row: Some(MultiBufferRow(0)),
32 diff_status: None,
33 expand_info: None,
34 }]
35 );
36}
37
38#[gpui::test]
39fn test_singleton(cx: &mut App) {
40 let buffer = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
41 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
42
43 let snapshot = multibuffer.read(cx).snapshot(cx);
44 assert_eq!(snapshot.text(), buffer.read(cx).text());
45
46 assert_eq!(
47 snapshot
48 .row_infos(MultiBufferRow(0))
49 .map(|info| info.buffer_row)
50 .collect::<Vec<_>>(),
51 (0..buffer.read(cx).row_count())
52 .map(Some)
53 .collect::<Vec<_>>()
54 );
55 assert_consistent_line_numbers(&snapshot);
56
57 buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx));
58 let snapshot = multibuffer.read(cx).snapshot(cx);
59
60 assert_eq!(snapshot.text(), buffer.read(cx).text());
61 assert_eq!(
62 snapshot
63 .row_infos(MultiBufferRow(0))
64 .map(|info| info.buffer_row)
65 .collect::<Vec<_>>(),
66 (0..buffer.read(cx).row_count())
67 .map(Some)
68 .collect::<Vec<_>>()
69 );
70 assert_consistent_line_numbers(&snapshot);
71}
72
73#[gpui::test]
74fn test_remote(cx: &mut App) {
75 let host_buffer = cx.new(|cx| Buffer::local("a", cx));
76 let guest_buffer = cx.new(|cx| {
77 let state = host_buffer.read(cx).to_proto(cx);
78 let ops = cx
79 .background_executor()
80 .block(host_buffer.read(cx).serialize_ops(None, cx));
81 let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
82 buffer.apply_ops(
83 ops.into_iter()
84 .map(|op| language::proto::deserialize_operation(op).unwrap()),
85 cx,
86 );
87 buffer
88 });
89 let multibuffer = cx.new(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx));
90 let snapshot = multibuffer.read(cx).snapshot(cx);
91 assert_eq!(snapshot.text(), "a");
92
93 guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx));
94 let snapshot = multibuffer.read(cx).snapshot(cx);
95 assert_eq!(snapshot.text(), "ab");
96
97 guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx));
98 let snapshot = multibuffer.read(cx).snapshot(cx);
99 assert_eq!(snapshot.text(), "abc");
100}
101
102#[gpui::test]
103fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
104 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
105 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
106 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
107
108 let events = Arc::new(RwLock::new(Vec::<Event>::new()));
109 multibuffer.update(cx, |_, cx| {
110 let events = events.clone();
111 cx.subscribe(&multibuffer, move |_, _, event, _| {
112 if let Event::Edited { .. } = event {
113 events.write().push(event.clone())
114 }
115 })
116 .detach();
117 });
118
119 let subscription = multibuffer.update(cx, |multibuffer, cx| {
120 let subscription = multibuffer.subscribe();
121 multibuffer.push_excerpts(
122 buffer_1.clone(),
123 [ExcerptRange {
124 context: Point::new(1, 2)..Point::new(2, 5),
125 primary: None,
126 }],
127 cx,
128 );
129 assert_eq!(
130 subscription.consume().into_inner(),
131 [Edit {
132 old: 0..0,
133 new: 0..10
134 }]
135 );
136
137 multibuffer.push_excerpts(
138 buffer_1.clone(),
139 [ExcerptRange {
140 context: Point::new(3, 3)..Point::new(4, 4),
141 primary: None,
142 }],
143 cx,
144 );
145 multibuffer.push_excerpts(
146 buffer_2.clone(),
147 [ExcerptRange {
148 context: Point::new(3, 1)..Point::new(3, 3),
149 primary: None,
150 }],
151 cx,
152 );
153 assert_eq!(
154 subscription.consume().into_inner(),
155 [Edit {
156 old: 10..10,
157 new: 10..22
158 }]
159 );
160
161 subscription
162 });
163
164 // Adding excerpts emits an edited event.
165 assert_eq!(
166 events.read().as_slice(),
167 &[
168 Event::Edited {
169 singleton_buffer_edited: false,
170 edited_buffer: None,
171 },
172 Event::Edited {
173 singleton_buffer_edited: false,
174 edited_buffer: None,
175 },
176 Event::Edited {
177 singleton_buffer_edited: false,
178 edited_buffer: None,
179 }
180 ]
181 );
182
183 let snapshot = multibuffer.read(cx).snapshot(cx);
184 assert_eq!(
185 snapshot.text(),
186 indoc!(
187 "
188 bbbb
189 ccccc
190 ddd
191 eeee
192 jj"
193 ),
194 );
195 assert_eq!(
196 snapshot
197 .row_infos(MultiBufferRow(0))
198 .map(|info| info.buffer_row)
199 .collect::<Vec<_>>(),
200 [Some(1), Some(2), Some(3), Some(4), Some(3)]
201 );
202 assert_eq!(
203 snapshot
204 .row_infos(MultiBufferRow(2))
205 .map(|info| info.buffer_row)
206 .collect::<Vec<_>>(),
207 [Some(3), Some(4), Some(3)]
208 );
209 assert_eq!(
210 snapshot
211 .row_infos(MultiBufferRow(4))
212 .map(|info| info.buffer_row)
213 .collect::<Vec<_>>(),
214 [Some(3)]
215 );
216 assert_eq!(
217 snapshot
218 .row_infos(MultiBufferRow(5))
219 .map(|info| info.buffer_row)
220 .collect::<Vec<_>>(),
221 []
222 );
223
224 assert_eq!(
225 boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot),
226 &[
227 (MultiBufferRow(0), "bbbb\nccccc".to_string(), true),
228 (MultiBufferRow(2), "ddd\neeee".to_string(), false),
229 (MultiBufferRow(4), "jj".to_string(), true),
230 ]
231 );
232 assert_eq!(
233 boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot),
234 &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)]
235 );
236 assert_eq!(
237 boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot),
238 &[]
239 );
240 assert_eq!(
241 boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot),
242 &[]
243 );
244 assert_eq!(
245 boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
246 &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)]
247 );
248 assert_eq!(
249 boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
250 &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)]
251 );
252 assert_eq!(
253 boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot),
254 &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)]
255 );
256 assert_eq!(
257 boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot),
258 &[(MultiBufferRow(4), "jj".to_string(), true)]
259 );
260 assert_eq!(
261 boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot),
262 &[]
263 );
264
265 buffer_1.update(cx, |buffer, cx| {
266 let text = "\n";
267 buffer.edit(
268 [
269 (Point::new(0, 0)..Point::new(0, 0), text),
270 (Point::new(2, 1)..Point::new(2, 3), text),
271 ],
272 None,
273 cx,
274 );
275 });
276
277 let snapshot = multibuffer.read(cx).snapshot(cx);
278 assert_eq!(
279 snapshot.text(),
280 concat!(
281 "bbbb\n", // Preserve newlines
282 "c\n", //
283 "cc\n", //
284 "ddd\n", //
285 "eeee\n", //
286 "jj" //
287 )
288 );
289
290 assert_eq!(
291 subscription.consume().into_inner(),
292 [Edit {
293 old: 6..8,
294 new: 6..7
295 }]
296 );
297
298 let snapshot = multibuffer.read(cx).snapshot(cx);
299 assert_eq!(
300 snapshot.clip_point(Point::new(0, 5), Bias::Left),
301 Point::new(0, 4)
302 );
303 assert_eq!(
304 snapshot.clip_point(Point::new(0, 5), Bias::Right),
305 Point::new(0, 4)
306 );
307 assert_eq!(
308 snapshot.clip_point(Point::new(5, 1), Bias::Right),
309 Point::new(5, 1)
310 );
311 assert_eq!(
312 snapshot.clip_point(Point::new(5, 2), Bias::Right),
313 Point::new(5, 2)
314 );
315 assert_eq!(
316 snapshot.clip_point(Point::new(5, 3), Bias::Right),
317 Point::new(5, 2)
318 );
319
320 let snapshot = multibuffer.update(cx, |multibuffer, cx| {
321 let (buffer_2_excerpt_id, _) =
322 multibuffer.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)[0].clone();
323 multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
324 multibuffer.snapshot(cx)
325 });
326
327 assert_eq!(
328 snapshot.text(),
329 concat!(
330 "bbbb\n", // Preserve newlines
331 "c\n", //
332 "cc\n", //
333 "ddd\n", //
334 "eeee", //
335 )
336 );
337
338 fn boundaries_in_range(
339 range: Range<Point>,
340 snapshot: &MultiBufferSnapshot,
341 ) -> Vec<(MultiBufferRow, String, bool)> {
342 snapshot
343 .excerpt_boundaries_in_range(range)
344 .map(|boundary| {
345 let starts_new_buffer = boundary.starts_new_buffer();
346 (
347 boundary.row,
348 boundary
349 .next
350 .buffer
351 .text_for_range(boundary.next.range.context)
352 .collect::<String>(),
353 starts_new_buffer,
354 )
355 })
356 .collect::<Vec<_>>()
357 }
358}
359
360#[gpui::test]
361fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
362 let base_text = "one\ntwo\nthree\n";
363 let text = "one\nthree\n";
364 let buffer = cx.new(|cx| Buffer::local(text, cx));
365 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
366 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
367 multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx));
368
369 let (before, after) = multibuffer.update(cx, |multibuffer, cx| {
370 let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0));
371 let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0));
372 multibuffer.set_all_diff_hunks_expanded(cx);
373 (before, after)
374 });
375 cx.run_until_parked();
376
377 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
378 let actual_text = snapshot.text();
379 let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
380 let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None);
381 pretty_assertions::assert_eq!(
382 actual_diff,
383 indoc! {
384 " one
385 - two
386 three
387 "
388 },
389 );
390
391 multibuffer.update(cx, |multibuffer, cx| {
392 let snapshot = multibuffer.snapshot(cx);
393 assert_eq!(before.to_point(&snapshot), Point::new(1, 0));
394 assert_eq!(after.to_point(&snapshot), Point::new(2, 0));
395 assert_eq!(
396 vec![Point::new(1, 0), Point::new(2, 0),],
397 snapshot.summaries_for_anchors::<Point, _>(&[before, after]),
398 )
399 })
400}
401
402#[gpui::test]
403fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
404 let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
405 let text = "one\nfour\nseven\n";
406 let buffer = cx.new(|cx| Buffer::local(text, cx));
407 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
408 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
409 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
410 (multibuffer.snapshot(cx), multibuffer.subscribe())
411 });
412
413 multibuffer.update(cx, |multibuffer, cx| {
414 multibuffer.add_diff(diff, cx);
415 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
416 });
417
418 assert_new_snapshot(
419 &multibuffer,
420 &mut snapshot,
421 &mut subscription,
422 cx,
423 indoc! {
424 " one
425 - two
426 - three
427 four
428 - five
429 - six
430 seven
431 - eight
432 "
433 },
434 );
435
436 assert_eq!(
437 snapshot
438 .diff_hunks_in_range(Point::new(1, 0)..Point::MAX)
439 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
440 .collect::<Vec<_>>(),
441 vec![1..3, 4..6, 7..8]
442 );
443
444 assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,);
445 assert_eq!(
446 snapshot.diff_hunk_before(Point::new(7, 0)),
447 Some(MultiBufferRow(4))
448 );
449 assert_eq!(
450 snapshot.diff_hunk_before(Point::new(4, 0)),
451 Some(MultiBufferRow(1))
452 );
453
454 multibuffer.update(cx, |multibuffer, cx| {
455 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
456 });
457
458 assert_new_snapshot(
459 &multibuffer,
460 &mut snapshot,
461 &mut subscription,
462 cx,
463 indoc! {
464 "
465 one
466 four
467 seven
468 "
469 },
470 );
471
472 assert_eq!(
473 snapshot.diff_hunk_before(Point::new(2, 0)),
474 Some(MultiBufferRow(1)),
475 );
476 assert_eq!(
477 snapshot.diff_hunk_before(Point::new(4, 0)),
478 Some(MultiBufferRow(2))
479 );
480}
481
482#[gpui::test]
483fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
484 let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
485 let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
486 let buffer = cx.new(|cx| Buffer::local(text, cx));
487 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
488 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
489
490 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
491 multibuffer.add_diff(diff.clone(), cx);
492 (multibuffer.snapshot(cx), multibuffer.subscribe())
493 });
494
495 cx.executor().run_until_parked();
496 multibuffer.update(cx, |multibuffer, cx| {
497 multibuffer.set_all_diff_hunks_expanded(cx);
498 });
499
500 assert_new_snapshot(
501 &multibuffer,
502 &mut snapshot,
503 &mut subscription,
504 cx,
505 indoc! {
506 "
507 one
508 two
509 + THREE
510 four
511 five
512 - six
513 seven
514 "
515 },
516 );
517
518 // Insert a newline within an insertion hunk
519 multibuffer.update(cx, |multibuffer, cx| {
520 multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx);
521 });
522 assert_new_snapshot(
523 &multibuffer,
524 &mut snapshot,
525 &mut subscription,
526 cx,
527 indoc! {
528 "
529 one
530 two
531 + __
532 + __THREE
533 four
534 five
535 - six
536 seven
537 "
538 },
539 );
540
541 // Delete the newline before a deleted hunk.
542 multibuffer.update(cx, |multibuffer, cx| {
543 multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx);
544 });
545 assert_new_snapshot(
546 &multibuffer,
547 &mut snapshot,
548 &mut subscription,
549 cx,
550 indoc! {
551 "
552 one
553 two
554 + __
555 + __THREE
556 four
557 fiveseven
558 "
559 },
560 );
561
562 multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx));
563 assert_new_snapshot(
564 &multibuffer,
565 &mut snapshot,
566 &mut subscription,
567 cx,
568 indoc! {
569 "
570 one
571 two
572 + __
573 + __THREE
574 four
575 five
576 - six
577 seven
578 "
579 },
580 );
581
582 // Cannot (yet) insert at the beginning of a deleted hunk.
583 // (because it would put the newline in the wrong place)
584 multibuffer.update(cx, |multibuffer, cx| {
585 multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx);
586 });
587 assert_new_snapshot(
588 &multibuffer,
589 &mut snapshot,
590 &mut subscription,
591 cx,
592 indoc! {
593 "
594 one
595 two
596 + __
597 + __THREE
598 four
599 five
600 - six
601 seven
602 "
603 },
604 );
605
606 // Replace a range that ends in a deleted hunk.
607 multibuffer.update(cx, |multibuffer, cx| {
608 multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx);
609 });
610 assert_new_snapshot(
611 &multibuffer,
612 &mut snapshot,
613 &mut subscription,
614 cx,
615 indoc! {
616 "
617 one
618 two
619 + __
620 + __THREE
621 four
622 fifty-seven
623 "
624 },
625 );
626}
627
628#[gpui::test]
629fn test_excerpt_events(cx: &mut App) {
630 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'a'), cx));
631 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'm'), cx));
632
633 let leader_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
634 let follower_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
635 let follower_edit_event_count = Arc::new(RwLock::new(0));
636
637 follower_multibuffer.update(cx, |_, cx| {
638 let follower_edit_event_count = follower_edit_event_count.clone();
639 cx.subscribe(
640 &leader_multibuffer,
641 move |follower, _, event, cx| match event.clone() {
642 Event::ExcerptsAdded {
643 buffer,
644 predecessor,
645 excerpts,
646 } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
647 Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
648 Event::Edited { .. } => {
649 *follower_edit_event_count.write() += 1;
650 }
651 _ => {}
652 },
653 )
654 .detach();
655 });
656
657 leader_multibuffer.update(cx, |leader, cx| {
658 leader.push_excerpts(
659 buffer_1.clone(),
660 [
661 ExcerptRange {
662 context: 0..8,
663 primary: None,
664 },
665 ExcerptRange {
666 context: 12..16,
667 primary: None,
668 },
669 ],
670 cx,
671 );
672 leader.insert_excerpts_after(
673 leader.excerpt_ids()[0],
674 buffer_2.clone(),
675 [
676 ExcerptRange {
677 context: 0..5,
678 primary: None,
679 },
680 ExcerptRange {
681 context: 10..15,
682 primary: None,
683 },
684 ],
685 cx,
686 )
687 });
688 assert_eq!(
689 leader_multibuffer.read(cx).snapshot(cx).text(),
690 follower_multibuffer.read(cx).snapshot(cx).text(),
691 );
692 assert_eq!(*follower_edit_event_count.read(), 2);
693
694 leader_multibuffer.update(cx, |leader, cx| {
695 let excerpt_ids = leader.excerpt_ids();
696 leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx);
697 });
698 assert_eq!(
699 leader_multibuffer.read(cx).snapshot(cx).text(),
700 follower_multibuffer.read(cx).snapshot(cx).text(),
701 );
702 assert_eq!(*follower_edit_event_count.read(), 3);
703
704 // Removing an empty set of excerpts is a noop.
705 leader_multibuffer.update(cx, |leader, cx| {
706 leader.remove_excerpts([], cx);
707 });
708 assert_eq!(
709 leader_multibuffer.read(cx).snapshot(cx).text(),
710 follower_multibuffer.read(cx).snapshot(cx).text(),
711 );
712 assert_eq!(*follower_edit_event_count.read(), 3);
713
714 // Adding an empty set of excerpts is a noop.
715 leader_multibuffer.update(cx, |leader, cx| {
716 leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
717 });
718 assert_eq!(
719 leader_multibuffer.read(cx).snapshot(cx).text(),
720 follower_multibuffer.read(cx).snapshot(cx).text(),
721 );
722 assert_eq!(*follower_edit_event_count.read(), 3);
723
724 leader_multibuffer.update(cx, |leader, cx| {
725 leader.clear(cx);
726 });
727 assert_eq!(
728 leader_multibuffer.read(cx).snapshot(cx).text(),
729 follower_multibuffer.read(cx).snapshot(cx).text(),
730 );
731 assert_eq!(*follower_edit_event_count.read(), 4);
732}
733
734#[gpui::test]
735fn test_expand_excerpts(cx: &mut App) {
736 let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
737 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
738
739 multibuffer.update(cx, |multibuffer, cx| {
740 multibuffer.push_excerpts_with_context_lines(
741 buffer.clone(),
742 vec![
743 // Note that in this test, this first excerpt
744 // does not contain a new line
745 Point::new(3, 2)..Point::new(3, 3),
746 Point::new(7, 1)..Point::new(7, 3),
747 Point::new(15, 0)..Point::new(15, 0),
748 ],
749 1,
750 cx,
751 )
752 });
753
754 let snapshot = multibuffer.read(cx).snapshot(cx);
755
756 assert_eq!(
757 snapshot.text(),
758 concat!(
759 "ccc\n", //
760 "ddd\n", //
761 "eee", //
762 "\n", // End of excerpt
763 "ggg\n", //
764 "hhh\n", //
765 "iii", //
766 "\n", // End of excerpt
767 "ooo\n", //
768 "ppp\n", //
769 "qqq", // End of excerpt
770 )
771 );
772 drop(snapshot);
773
774 multibuffer.update(cx, |multibuffer, cx| {
775 multibuffer.expand_excerpts(
776 multibuffer.excerpt_ids(),
777 1,
778 ExpandExcerptDirection::UpAndDown,
779 cx,
780 )
781 });
782
783 let snapshot = multibuffer.read(cx).snapshot(cx);
784
785 // Expanding context lines causes the line containing 'fff' to appear in two different excerpts.
786 // We don't attempt to merge them, because removing the excerpt could create inconsistency with other layers
787 // that are tracking excerpt ids.
788 assert_eq!(
789 snapshot.text(),
790 concat!(
791 "bbb\n", //
792 "ccc\n", //
793 "ddd\n", //
794 "eee\n", //
795 "fff\n", // End of excerpt
796 "fff\n", //
797 "ggg\n", //
798 "hhh\n", //
799 "iii\n", //
800 "jjj\n", // End of excerpt
801 "nnn\n", //
802 "ooo\n", //
803 "ppp\n", //
804 "qqq\n", //
805 "rrr", // End of excerpt
806 )
807 );
808}
809
810#[gpui::test]
811fn test_push_excerpts_with_context_lines(cx: &mut App) {
812 let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
813 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
814 let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
815 multibuffer.push_excerpts_with_context_lines(
816 buffer.clone(),
817 vec![
818 // Note that in this test, this first excerpt
819 // does contain a new line
820 Point::new(3, 2)..Point::new(4, 2),
821 Point::new(7, 1)..Point::new(7, 3),
822 Point::new(15, 0)..Point::new(15, 0),
823 ],
824 2,
825 cx,
826 )
827 });
828
829 let snapshot = multibuffer.read(cx).snapshot(cx);
830 assert_eq!(
831 snapshot.text(),
832 concat!(
833 "bbb\n", // Preserve newlines
834 "ccc\n", //
835 "ddd\n", //
836 "eee\n", //
837 "fff\n", //
838 "ggg\n", //
839 "hhh\n", //
840 "iii\n", //
841 "jjj\n", //
842 "nnn\n", //
843 "ooo\n", //
844 "ppp\n", //
845 "qqq\n", //
846 "rrr", //
847 )
848 );
849
850 assert_eq!(
851 anchor_ranges
852 .iter()
853 .map(|range| range.to_point(&snapshot))
854 .collect::<Vec<_>>(),
855 vec![
856 Point::new(2, 2)..Point::new(3, 2),
857 Point::new(6, 1)..Point::new(6, 3),
858 Point::new(11, 0)..Point::new(11, 0)
859 ]
860 );
861}
862
863#[gpui::test(iterations = 100)]
864async fn test_push_multiple_excerpts_with_context_lines(cx: &mut TestAppContext) {
865 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
866 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(15, 4, 'a'), cx));
867 let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot());
868 let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot());
869 let ranges_1 = vec![
870 snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)),
871 snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)),
872 snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)),
873 ];
874 let ranges_2 = vec![
875 snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)),
876 snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)),
877 ];
878
879 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
880 let anchor_ranges = multibuffer
881 .update(cx, |multibuffer, cx| {
882 multibuffer.push_multiple_excerpts_with_context_lines(
883 vec![(buffer_1.clone(), ranges_1), (buffer_2.clone(), ranges_2)],
884 2,
885 cx,
886 )
887 })
888 .await;
889
890 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
891 assert_eq!(
892 snapshot.text(),
893 concat!(
894 "bbb\n", // buffer_1
895 "ccc\n", //
896 "ddd\n", // <-- excerpt 1
897 "eee\n", // <-- excerpt 1
898 "fff\n", //
899 "ggg\n", //
900 "hhh\n", // <-- excerpt 2
901 "iii\n", //
902 "jjj\n", //
903 //
904 "nnn\n", //
905 "ooo\n", //
906 "ppp\n", // <-- excerpt 3
907 "qqq\n", //
908 "rrr\n", //
909 //
910 "aaaa\n", // buffer 2
911 "bbbb\n", //
912 "cccc\n", // <-- excerpt 4
913 "dddd\n", // <-- excerpt 4
914 "eeee\n", //
915 "ffff\n", //
916 //
917 "iiii\n", //
918 "jjjj\n", //
919 "kkkk\n", // <-- excerpt 5
920 "llll\n", //
921 "mmmm", //
922 )
923 );
924
925 assert_eq!(
926 anchor_ranges
927 .iter()
928 .map(|range| range.to_point(&snapshot))
929 .collect::<Vec<_>>(),
930 vec![
931 Point::new(2, 2)..Point::new(3, 2),
932 Point::new(6, 1)..Point::new(6, 3),
933 Point::new(11, 0)..Point::new(11, 0),
934 Point::new(16, 1)..Point::new(17, 1),
935 Point::new(22, 0)..Point::new(22, 2)
936 ]
937 );
938}
939
940#[gpui::test]
941fn test_empty_multibuffer(cx: &mut App) {
942 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
943
944 let snapshot = multibuffer.read(cx).snapshot(cx);
945 assert_eq!(snapshot.text(), "");
946 assert_eq!(
947 snapshot
948 .row_infos(MultiBufferRow(0))
949 .map(|info| info.buffer_row)
950 .collect::<Vec<_>>(),
951 &[Some(0)]
952 );
953 assert_eq!(
954 snapshot
955 .row_infos(MultiBufferRow(1))
956 .map(|info| info.buffer_row)
957 .collect::<Vec<_>>(),
958 &[]
959 );
960}
961
962#[gpui::test]
963fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
964 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
965 let buffer = cx.new(|cx| Buffer::local("", cx));
966 let base_text = "a\nb\nc";
967
968 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
969 multibuffer.update(cx, |multibuffer, cx| {
970 multibuffer.push_excerpts(
971 buffer.clone(),
972 [ExcerptRange {
973 context: 0..0,
974 primary: None,
975 }],
976 cx,
977 );
978 multibuffer.set_all_diff_hunks_expanded(cx);
979 multibuffer.add_diff(diff.clone(), cx);
980 });
981 cx.run_until_parked();
982
983 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
984 assert_eq!(snapshot.text(), "a\nb\nc\n");
985
986 let hunk = snapshot
987 .diff_hunks_in_range(Point::new(1, 1)..Point::new(1, 1))
988 .next()
989 .unwrap();
990
991 assert_eq!(hunk.diff_base_byte_range.start, 0);
992
993 let buf2 = cx.new(|cx| Buffer::local("X", cx));
994 multibuffer.update(cx, |multibuffer, cx| {
995 multibuffer.push_excerpts(
996 buf2,
997 [ExcerptRange {
998 context: 0..1,
999 primary: None,
1000 }],
1001 cx,
1002 );
1003 });
1004
1005 buffer.update(cx, |buffer, cx| {
1006 buffer.edit([(0..0, "a\nb\nc")], None, cx);
1007 diff.update(cx, |diff, cx| {
1008 diff.recalculate_diff_sync(buffer.snapshot().text, cx);
1009 });
1010 assert_eq!(buffer.text(), "a\nb\nc")
1011 });
1012 cx.run_until_parked();
1013
1014 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
1015 assert_eq!(snapshot.text(), "a\nb\nc\nX");
1016
1017 buffer.update(cx, |buffer, cx| {
1018 buffer.undo(cx);
1019 diff.update(cx, |diff, cx| {
1020 diff.recalculate_diff_sync(buffer.snapshot().text, cx);
1021 });
1022 assert_eq!(buffer.text(), "")
1023 });
1024 cx.run_until_parked();
1025
1026 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
1027 assert_eq!(snapshot.text(), "a\nb\nc\n\nX");
1028}
1029
1030#[gpui::test]
1031fn test_singleton_multibuffer_anchors(cx: &mut App) {
1032 let buffer = cx.new(|cx| Buffer::local("abcd", cx));
1033 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1034 let old_snapshot = multibuffer.read(cx).snapshot(cx);
1035 buffer.update(cx, |buffer, cx| {
1036 buffer.edit([(0..0, "X")], None, cx);
1037 buffer.edit([(5..5, "Y")], None, cx);
1038 });
1039 let new_snapshot = multibuffer.read(cx).snapshot(cx);
1040
1041 assert_eq!(old_snapshot.text(), "abcd");
1042 assert_eq!(new_snapshot.text(), "XabcdY");
1043
1044 assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0);
1045 assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1);
1046 assert_eq!(old_snapshot.anchor_before(4).to_offset(&new_snapshot), 5);
1047 assert_eq!(old_snapshot.anchor_after(4).to_offset(&new_snapshot), 6);
1048}
1049
1050#[gpui::test]
1051fn test_multibuffer_anchors(cx: &mut App) {
1052 let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx));
1053 let buffer_2 = cx.new(|cx| Buffer::local("efghi", cx));
1054 let multibuffer = cx.new(|cx| {
1055 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1056 multibuffer.push_excerpts(
1057 buffer_1.clone(),
1058 [ExcerptRange {
1059 context: 0..4,
1060 primary: None,
1061 }],
1062 cx,
1063 );
1064 multibuffer.push_excerpts(
1065 buffer_2.clone(),
1066 [ExcerptRange {
1067 context: 0..5,
1068 primary: None,
1069 }],
1070 cx,
1071 );
1072 multibuffer
1073 });
1074 let old_snapshot = multibuffer.read(cx).snapshot(cx);
1075
1076 assert_eq!(old_snapshot.anchor_before(0).to_offset(&old_snapshot), 0);
1077 assert_eq!(old_snapshot.anchor_after(0).to_offset(&old_snapshot), 0);
1078 assert_eq!(Anchor::min().to_offset(&old_snapshot), 0);
1079 assert_eq!(Anchor::min().to_offset(&old_snapshot), 0);
1080 assert_eq!(Anchor::max().to_offset(&old_snapshot), 10);
1081 assert_eq!(Anchor::max().to_offset(&old_snapshot), 10);
1082
1083 buffer_1.update(cx, |buffer, cx| {
1084 buffer.edit([(0..0, "W")], None, cx);
1085 buffer.edit([(5..5, "X")], None, cx);
1086 });
1087 buffer_2.update(cx, |buffer, cx| {
1088 buffer.edit([(0..0, "Y")], None, cx);
1089 buffer.edit([(6..6, "Z")], None, cx);
1090 });
1091 let new_snapshot = multibuffer.read(cx).snapshot(cx);
1092
1093 assert_eq!(old_snapshot.text(), "abcd\nefghi");
1094 assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ");
1095
1096 assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0);
1097 assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1);
1098 assert_eq!(old_snapshot.anchor_before(1).to_offset(&new_snapshot), 2);
1099 assert_eq!(old_snapshot.anchor_after(1).to_offset(&new_snapshot), 2);
1100 assert_eq!(old_snapshot.anchor_before(2).to_offset(&new_snapshot), 3);
1101 assert_eq!(old_snapshot.anchor_after(2).to_offset(&new_snapshot), 3);
1102 assert_eq!(old_snapshot.anchor_before(5).to_offset(&new_snapshot), 7);
1103 assert_eq!(old_snapshot.anchor_after(5).to_offset(&new_snapshot), 8);
1104 assert_eq!(old_snapshot.anchor_before(10).to_offset(&new_snapshot), 13);
1105 assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14);
1106}
1107
1108#[gpui::test]
1109fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) {
1110 let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx));
1111 let buffer_2 = cx.new(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx));
1112 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1113
1114 // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
1115 // Add an excerpt from buffer 1 that spans this new insertion.
1116 buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx));
1117 let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| {
1118 multibuffer
1119 .push_excerpts(
1120 buffer_1.clone(),
1121 [ExcerptRange {
1122 context: 0..7,
1123 primary: None,
1124 }],
1125 cx,
1126 )
1127 .pop()
1128 .unwrap()
1129 });
1130
1131 let snapshot_1 = multibuffer.read(cx).snapshot(cx);
1132 assert_eq!(snapshot_1.text(), "abcd123");
1133
1134 // Replace the buffer 1 excerpt with new excerpts from buffer 2.
1135 let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
1136 multibuffer.remove_excerpts([excerpt_id_1], cx);
1137 let mut ids = multibuffer
1138 .push_excerpts(
1139 buffer_2.clone(),
1140 [
1141 ExcerptRange {
1142 context: 0..4,
1143 primary: None,
1144 },
1145 ExcerptRange {
1146 context: 6..10,
1147 primary: None,
1148 },
1149 ExcerptRange {
1150 context: 12..16,
1151 primary: None,
1152 },
1153 ],
1154 cx,
1155 )
1156 .into_iter();
1157 (ids.next().unwrap(), ids.next().unwrap())
1158 });
1159 let snapshot_2 = multibuffer.read(cx).snapshot(cx);
1160 assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP");
1161
1162 // The old excerpt id doesn't get reused.
1163 assert_ne!(excerpt_id_2, excerpt_id_1);
1164
1165 // Resolve some anchors from the previous snapshot in the new snapshot.
1166 // The current excerpts are from a different buffer, so we don't attempt to
1167 // resolve the old text anchor in the new buffer.
1168 assert_eq!(
1169 snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
1170 0
1171 );
1172 assert_eq!(
1173 snapshot_2.summaries_for_anchors::<usize, _>(&[
1174 snapshot_1.anchor_before(2),
1175 snapshot_1.anchor_after(3)
1176 ]),
1177 vec![0, 0]
1178 );
1179
1180 // Refresh anchors from the old snapshot. The return value indicates that both
1181 // anchors lost their original excerpt.
1182 let refresh =
1183 snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
1184 assert_eq!(
1185 refresh,
1186 &[
1187 (0, snapshot_2.anchor_before(0), false),
1188 (1, snapshot_2.anchor_after(0), false),
1189 ]
1190 );
1191
1192 // Replace the middle excerpt with a smaller excerpt in buffer 2,
1193 // that intersects the old excerpt.
1194 let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
1195 multibuffer.remove_excerpts([excerpt_id_3], cx);
1196 multibuffer
1197 .insert_excerpts_after(
1198 excerpt_id_2,
1199 buffer_2.clone(),
1200 [ExcerptRange {
1201 context: 5..8,
1202 primary: None,
1203 }],
1204 cx,
1205 )
1206 .pop()
1207 .unwrap()
1208 });
1209
1210 let snapshot_3 = multibuffer.read(cx).snapshot(cx);
1211 assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP");
1212 assert_ne!(excerpt_id_5, excerpt_id_3);
1213
1214 // Resolve some anchors from the previous snapshot in the new snapshot.
1215 // The third anchor can't be resolved, since its excerpt has been removed,
1216 // so it resolves to the same position as its predecessor.
1217 let anchors = [
1218 snapshot_2.anchor_before(0),
1219 snapshot_2.anchor_after(2),
1220 snapshot_2.anchor_after(6),
1221 snapshot_2.anchor_after(14),
1222 ];
1223 assert_eq!(
1224 snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
1225 &[0, 2, 9, 13]
1226 );
1227
1228 let new_anchors = snapshot_3.refresh_anchors(&anchors);
1229 assert_eq!(
1230 new_anchors.iter().map(|a| (a.0, a.2)).collect::<Vec<_>>(),
1231 &[(0, true), (1, true), (2, true), (3, true)]
1232 );
1233 assert_eq!(
1234 snapshot_3.summaries_for_anchors::<usize, _>(new_anchors.iter().map(|a| &a.1)),
1235 &[0, 2, 7, 13]
1236 );
1237}
1238
1239#[gpui::test]
1240fn test_basic_diff_hunks(cx: &mut TestAppContext) {
1241 let text = indoc!(
1242 "
1243 ZERO
1244 one
1245 TWO
1246 three
1247 six
1248 "
1249 );
1250 let base_text = indoc!(
1251 "
1252 one
1253 two
1254 three
1255 four
1256 five
1257 six
1258 "
1259 );
1260
1261 let buffer = cx.new(|cx| Buffer::local(text, cx));
1262 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
1263 cx.run_until_parked();
1264
1265 let multibuffer = cx.new(|cx| {
1266 let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
1267 multibuffer.add_diff(diff.clone(), cx);
1268 multibuffer
1269 });
1270
1271 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
1272 (multibuffer.snapshot(cx), multibuffer.subscribe())
1273 });
1274 assert_eq!(
1275 snapshot.text(),
1276 indoc!(
1277 "
1278 ZERO
1279 one
1280 TWO
1281 three
1282 six
1283 "
1284 ),
1285 );
1286
1287 multibuffer.update(cx, |multibuffer, cx| {
1288 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1289 });
1290
1291 assert_new_snapshot(
1292 &multibuffer,
1293 &mut snapshot,
1294 &mut subscription,
1295 cx,
1296 indoc!(
1297 "
1298 + ZERO
1299 one
1300 - two
1301 + TWO
1302 three
1303 - four
1304 - five
1305 six
1306 "
1307 ),
1308 );
1309
1310 assert_eq!(
1311 snapshot
1312 .row_infos(MultiBufferRow(0))
1313 .map(|info| (info.buffer_row, info.diff_status))
1314 .collect::<Vec<_>>(),
1315 vec![
1316 (Some(0), Some(DiffHunkStatus::added_none())),
1317 (Some(1), None),
1318 (Some(1), Some(DiffHunkStatus::deleted_none())),
1319 (Some(2), Some(DiffHunkStatus::added_none())),
1320 (Some(3), None),
1321 (Some(3), Some(DiffHunkStatus::deleted_none())),
1322 (Some(4), Some(DiffHunkStatus::deleted_none())),
1323 (Some(4), None),
1324 (Some(5), None)
1325 ]
1326 );
1327
1328 assert_chunks_in_ranges(&snapshot);
1329 assert_consistent_line_numbers(&snapshot);
1330 assert_position_translation(&snapshot);
1331 assert_line_indents(&snapshot);
1332
1333 multibuffer.update(cx, |multibuffer, cx| {
1334 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx)
1335 });
1336 assert_new_snapshot(
1337 &multibuffer,
1338 &mut snapshot,
1339 &mut subscription,
1340 cx,
1341 indoc!(
1342 "
1343 ZERO
1344 one
1345 TWO
1346 three
1347 six
1348 "
1349 ),
1350 );
1351
1352 assert_chunks_in_ranges(&snapshot);
1353 assert_consistent_line_numbers(&snapshot);
1354 assert_position_translation(&snapshot);
1355 assert_line_indents(&snapshot);
1356
1357 // Expand the first diff hunk
1358 multibuffer.update(cx, |multibuffer, cx| {
1359 let position = multibuffer.read(cx).anchor_before(Point::new(2, 2));
1360 multibuffer.expand_diff_hunks(vec![position..position], cx)
1361 });
1362 assert_new_snapshot(
1363 &multibuffer,
1364 &mut snapshot,
1365 &mut subscription,
1366 cx,
1367 indoc!(
1368 "
1369 ZERO
1370 one
1371 - two
1372 + TWO
1373 three
1374 six
1375 "
1376 ),
1377 );
1378
1379 // Expand the second diff hunk
1380 multibuffer.update(cx, |multibuffer, cx| {
1381 let start = multibuffer.read(cx).anchor_before(Point::new(4, 0));
1382 let end = multibuffer.read(cx).anchor_before(Point::new(5, 0));
1383 multibuffer.expand_diff_hunks(vec![start..end], cx)
1384 });
1385 assert_new_snapshot(
1386 &multibuffer,
1387 &mut snapshot,
1388 &mut subscription,
1389 cx,
1390 indoc!(
1391 "
1392 ZERO
1393 one
1394 - two
1395 + TWO
1396 three
1397 - four
1398 - five
1399 six
1400 "
1401 ),
1402 );
1403
1404 assert_chunks_in_ranges(&snapshot);
1405 assert_consistent_line_numbers(&snapshot);
1406 assert_position_translation(&snapshot);
1407 assert_line_indents(&snapshot);
1408
1409 // Edit the buffer before the first hunk
1410 buffer.update(cx, |buffer, cx| {
1411 buffer.edit_via_marked_text(
1412 indoc!(
1413 "
1414 ZERO
1415 one« hundred
1416 thousand»
1417 TWO
1418 three
1419 six
1420 "
1421 ),
1422 None,
1423 cx,
1424 );
1425 });
1426 assert_new_snapshot(
1427 &multibuffer,
1428 &mut snapshot,
1429 &mut subscription,
1430 cx,
1431 indoc!(
1432 "
1433 ZERO
1434 one hundred
1435 thousand
1436 - two
1437 + TWO
1438 three
1439 - four
1440 - five
1441 six
1442 "
1443 ),
1444 );
1445
1446 assert_chunks_in_ranges(&snapshot);
1447 assert_consistent_line_numbers(&snapshot);
1448 assert_position_translation(&snapshot);
1449 assert_line_indents(&snapshot);
1450
1451 // Recalculate the diff, changing the first diff hunk.
1452 diff.update(cx, |diff, cx| {
1453 diff.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx);
1454 });
1455 cx.run_until_parked();
1456 assert_new_snapshot(
1457 &multibuffer,
1458 &mut snapshot,
1459 &mut subscription,
1460 cx,
1461 indoc!(
1462 "
1463 ZERO
1464 one hundred
1465 thousand
1466 TWO
1467 three
1468 - four
1469 - five
1470 six
1471 "
1472 ),
1473 );
1474
1475 assert_eq!(
1476 snapshot
1477 .diff_hunks_in_range(0..snapshot.len())
1478 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
1479 .collect::<Vec<_>>(),
1480 &[0..4, 5..7]
1481 );
1482}
1483
1484#[gpui::test]
1485fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
1486 let text = indoc!(
1487 "
1488 one
1489 TWO
1490 THREE
1491 four
1492 FIVE
1493 six
1494 "
1495 );
1496 let base_text = indoc!(
1497 "
1498 one
1499 four
1500 five
1501 six
1502 "
1503 );
1504
1505 let buffer = cx.new(|cx| Buffer::local(text, cx));
1506 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
1507 cx.run_until_parked();
1508
1509 let multibuffer = cx.new(|cx| {
1510 let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
1511 multibuffer.add_diff(diff.clone(), cx);
1512 multibuffer
1513 });
1514
1515 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
1516 (multibuffer.snapshot(cx), multibuffer.subscribe())
1517 });
1518
1519 multibuffer.update(cx, |multibuffer, cx| {
1520 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1521 });
1522
1523 assert_new_snapshot(
1524 &multibuffer,
1525 &mut snapshot,
1526 &mut subscription,
1527 cx,
1528 indoc!(
1529 "
1530 one
1531 + TWO
1532 + THREE
1533 four
1534 - five
1535 + FIVE
1536 six
1537 "
1538 ),
1539 );
1540
1541 // Regression test: expanding diff hunks that are already expanded should not change anything.
1542 multibuffer.update(cx, |multibuffer, cx| {
1543 multibuffer.expand_diff_hunks(
1544 vec![
1545 snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)),
1546 ],
1547 cx,
1548 );
1549 });
1550
1551 assert_new_snapshot(
1552 &multibuffer,
1553 &mut snapshot,
1554 &mut subscription,
1555 cx,
1556 indoc!(
1557 "
1558 one
1559 + TWO
1560 + THREE
1561 four
1562 - five
1563 + FIVE
1564 six
1565 "
1566 ),
1567 );
1568
1569 // Now collapse all diff hunks
1570 multibuffer.update(cx, |multibuffer, cx| {
1571 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1572 });
1573
1574 assert_new_snapshot(
1575 &multibuffer,
1576 &mut snapshot,
1577 &mut subscription,
1578 cx,
1579 indoc!(
1580 "
1581 one
1582 TWO
1583 THREE
1584 four
1585 FIVE
1586 six
1587 "
1588 ),
1589 );
1590
1591 // Expand the hunks again, but this time provide two ranges that are both within the same hunk
1592 // Target the first hunk which is between "one" and "four"
1593 multibuffer.update(cx, |multibuffer, cx| {
1594 multibuffer.expand_diff_hunks(
1595 vec![
1596 snapshot.anchor_before(Point::new(4, 0))..snapshot.anchor_before(Point::new(4, 0)),
1597 snapshot.anchor_before(Point::new(4, 2))..snapshot.anchor_before(Point::new(4, 2)),
1598 ],
1599 cx,
1600 );
1601 });
1602 assert_new_snapshot(
1603 &multibuffer,
1604 &mut snapshot,
1605 &mut subscription,
1606 cx,
1607 indoc!(
1608 "
1609 one
1610 TWO
1611 THREE
1612 four
1613 - five
1614 + FIVE
1615 six
1616 "
1617 ),
1618 );
1619}
1620
1621#[gpui::test]
1622fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
1623 let buf1 = cx.new(|cx| {
1624 Buffer::local(
1625 indoc! {
1626 "zero
1627 one
1628 two
1629 two.five
1630 three
1631 four
1632 five
1633 six
1634 seven
1635 eight
1636 nine
1637 ten
1638 eleven
1639 ",
1640 },
1641 cx,
1642 )
1643 });
1644 let path1: PathKey = PathKey::namespaced("0", Path::new("/").into());
1645
1646 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1647 multibuffer.update(cx, |multibuffer, cx| {
1648 multibuffer.set_excerpts_for_path(
1649 path1.clone(),
1650 buf1.clone(),
1651 vec![
1652 Point::row_range(1..2),
1653 Point::row_range(6..7),
1654 Point::row_range(11..12),
1655 ],
1656 1,
1657 cx,
1658 );
1659 });
1660
1661 assert_excerpts_match(
1662 &multibuffer,
1663 cx,
1664 indoc! {
1665 "-----
1666 zero
1667 one
1668 two
1669 two.five
1670 -----
1671 four
1672 five
1673 six
1674 seven
1675 -----
1676 nine
1677 ten
1678 eleven
1679 "
1680 },
1681 );
1682
1683 buf1.update(cx, |buffer, cx| buffer.edit([(0..5, "")], None, cx));
1684
1685 multibuffer.update(cx, |multibuffer, cx| {
1686 multibuffer.set_excerpts_for_path(
1687 path1.clone(),
1688 buf1.clone(),
1689 vec![
1690 Point::row_range(0..2),
1691 Point::row_range(5..6),
1692 Point::row_range(10..11),
1693 ],
1694 1,
1695 cx,
1696 );
1697 });
1698
1699 assert_excerpts_match(
1700 &multibuffer,
1701 cx,
1702 indoc! {
1703 "-----
1704 one
1705 two
1706 two.five
1707 three
1708 -----
1709 four
1710 five
1711 six
1712 seven
1713 -----
1714 nine
1715 ten
1716 eleven
1717 "
1718 },
1719 );
1720}
1721
1722#[gpui::test]
1723fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
1724 let buf1 = cx.new(|cx| {
1725 Buffer::local(
1726 indoc! {
1727 "zero
1728 one
1729 two
1730 three
1731 four
1732 five
1733 six
1734 seven
1735 ",
1736 },
1737 cx,
1738 )
1739 });
1740 let path1: PathKey = PathKey::namespaced("0", Path::new("/").into());
1741 let buf2 = cx.new(|cx| {
1742 Buffer::local(
1743 indoc! {
1744 "000
1745 111
1746 222
1747 333
1748 444
1749 555
1750 666
1751 777
1752 888
1753 999
1754 "
1755 },
1756 cx,
1757 )
1758 });
1759 let path2 = PathKey::namespaced("x", Path::new("/").into());
1760
1761 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1762 multibuffer.update(cx, |multibuffer, cx| {
1763 multibuffer.set_excerpts_for_path(
1764 path1.clone(),
1765 buf1.clone(),
1766 vec![Point::row_range(0..1)],
1767 2,
1768 cx,
1769 );
1770 });
1771
1772 assert_excerpts_match(
1773 &multibuffer,
1774 cx,
1775 indoc! {
1776 "-----
1777 zero
1778 one
1779 two
1780 three
1781 "
1782 },
1783 );
1784
1785 multibuffer.update(cx, |multibuffer, cx| {
1786 multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
1787 });
1788
1789 assert_excerpts_match(&multibuffer, cx, "");
1790
1791 multibuffer.update(cx, |multibuffer, cx| {
1792 multibuffer.set_excerpts_for_path(
1793 path1.clone(),
1794 buf1.clone(),
1795 vec![Point::row_range(0..1), Point::row_range(7..8)],
1796 2,
1797 cx,
1798 );
1799 });
1800
1801 assert_excerpts_match(
1802 &multibuffer,
1803 cx,
1804 indoc! {"-----
1805 zero
1806 one
1807 two
1808 three
1809 -----
1810 five
1811 six
1812 seven
1813 "},
1814 );
1815
1816 multibuffer.update(cx, |multibuffer, cx| {
1817 multibuffer.set_excerpts_for_path(
1818 path1.clone(),
1819 buf1.clone(),
1820 vec![Point::row_range(0..1), Point::row_range(5..6)],
1821 2,
1822 cx,
1823 );
1824 });
1825
1826 assert_excerpts_match(
1827 &multibuffer,
1828 cx,
1829 indoc! {"-----
1830 zero
1831 one
1832 two
1833 three
1834 four
1835 five
1836 six
1837 seven
1838 "},
1839 );
1840
1841 multibuffer.update(cx, |multibuffer, cx| {
1842 multibuffer.set_excerpts_for_path(
1843 path2.clone(),
1844 buf2.clone(),
1845 vec![Point::row_range(2..3)],
1846 2,
1847 cx,
1848 );
1849 });
1850
1851 assert_excerpts_match(
1852 &multibuffer,
1853 cx,
1854 indoc! {"-----
1855 zero
1856 one
1857 two
1858 three
1859 four
1860 five
1861 six
1862 seven
1863 -----
1864 000
1865 111
1866 222
1867 333
1868 444
1869 555
1870 "},
1871 );
1872
1873 multibuffer.update(cx, |multibuffer, cx| {
1874 multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
1875 });
1876
1877 multibuffer.update(cx, |multibuffer, cx| {
1878 multibuffer.set_excerpts_for_path(
1879 path1.clone(),
1880 buf1.clone(),
1881 vec![Point::row_range(3..4)],
1882 2,
1883 cx,
1884 );
1885 });
1886
1887 assert_excerpts_match(
1888 &multibuffer,
1889 cx,
1890 indoc! {"-----
1891 one
1892 two
1893 three
1894 four
1895 five
1896 six
1897 -----
1898 000
1899 111
1900 222
1901 333
1902 444
1903 555
1904 "},
1905 );
1906
1907 multibuffer.update(cx, |multibuffer, cx| {
1908 multibuffer.set_excerpts_for_path(
1909 path1.clone(),
1910 buf1.clone(),
1911 vec![Point::row_range(3..4)],
1912 2,
1913 cx,
1914 );
1915 });
1916}
1917
1918#[gpui::test]
1919fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
1920 let base_text_1 = indoc!(
1921 "
1922 one
1923 two
1924 three
1925 four
1926 five
1927 six
1928 "
1929 );
1930 let text_1 = indoc!(
1931 "
1932 ZERO
1933 one
1934 TWO
1935 three
1936 six
1937 "
1938 );
1939 let base_text_2 = indoc!(
1940 "
1941 seven
1942 eight
1943 nine
1944 ten
1945 eleven
1946 twelve
1947 "
1948 );
1949 let text_2 = indoc!(
1950 "
1951 eight
1952 nine
1953 eleven
1954 THIRTEEN
1955 FOURTEEN
1956 "
1957 );
1958
1959 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
1960 let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
1961 let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx));
1962 let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx));
1963 cx.run_until_parked();
1964
1965 let multibuffer = cx.new(|cx| {
1966 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1967 multibuffer.push_excerpts(
1968 buffer_1.clone(),
1969 [ExcerptRange {
1970 context: text::Anchor::MIN..text::Anchor::MAX,
1971 primary: None,
1972 }],
1973 cx,
1974 );
1975 multibuffer.push_excerpts(
1976 buffer_2.clone(),
1977 [ExcerptRange {
1978 context: text::Anchor::MIN..text::Anchor::MAX,
1979 primary: None,
1980 }],
1981 cx,
1982 );
1983 multibuffer.add_diff(diff_1.clone(), cx);
1984 multibuffer.add_diff(diff_2.clone(), cx);
1985 multibuffer
1986 });
1987
1988 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
1989 (multibuffer.snapshot(cx), multibuffer.subscribe())
1990 });
1991 assert_eq!(
1992 snapshot.text(),
1993 indoc!(
1994 "
1995 ZERO
1996 one
1997 TWO
1998 three
1999 six
2000
2001 eight
2002 nine
2003 eleven
2004 THIRTEEN
2005 FOURTEEN
2006 "
2007 ),
2008 );
2009
2010 multibuffer.update(cx, |multibuffer, cx| {
2011 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
2012 });
2013
2014 assert_new_snapshot(
2015 &multibuffer,
2016 &mut snapshot,
2017 &mut subscription,
2018 cx,
2019 indoc!(
2020 "
2021 + ZERO
2022 one
2023 - two
2024 + TWO
2025 three
2026 - four
2027 - five
2028 six
2029
2030 - seven
2031 eight
2032 nine
2033 - ten
2034 eleven
2035 - twelve
2036 + THIRTEEN
2037 + FOURTEEN
2038 "
2039 ),
2040 );
2041
2042 let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
2043 let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
2044 let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().remote_id());
2045 let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().remote_id());
2046
2047 let buffer_lines = (0..=snapshot.max_row().0)
2048 .map(|row| {
2049 let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?;
2050 Some((
2051 buffer.remote_id(),
2052 buffer.text_for_range(range).collect::<String>(),
2053 ))
2054 })
2055 .collect::<Vec<_>>();
2056 pretty_assertions::assert_eq!(
2057 buffer_lines,
2058 [
2059 Some((id_1, "ZERO".into())),
2060 Some((id_1, "one".into())),
2061 Some((base_id_1, "two".into())),
2062 Some((id_1, "TWO".into())),
2063 Some((id_1, " three".into())),
2064 Some((base_id_1, "four".into())),
2065 Some((base_id_1, "five".into())),
2066 Some((id_1, "six".into())),
2067 Some((id_1, "".into())),
2068 Some((base_id_2, "seven".into())),
2069 Some((id_2, " eight".into())),
2070 Some((id_2, "nine".into())),
2071 Some((base_id_2, "ten".into())),
2072 Some((id_2, "eleven".into())),
2073 Some((base_id_2, "twelve".into())),
2074 Some((id_2, "THIRTEEN".into())),
2075 Some((id_2, "FOURTEEN".into())),
2076 Some((id_2, "".into())),
2077 ]
2078 );
2079
2080 let buffer_ids_by_range = [
2081 (Point::new(0, 0)..Point::new(0, 0), &[id_1] as &[_]),
2082 (Point::new(0, 0)..Point::new(2, 0), &[id_1]),
2083 (Point::new(2, 0)..Point::new(2, 0), &[id_1]),
2084 (Point::new(3, 0)..Point::new(3, 0), &[id_1]),
2085 (Point::new(8, 0)..Point::new(9, 0), &[id_1]),
2086 (Point::new(8, 0)..Point::new(10, 0), &[id_1, id_2]),
2087 (Point::new(9, 0)..Point::new(9, 0), &[id_2]),
2088 ];
2089 for (range, buffer_ids) in buffer_ids_by_range {
2090 assert_eq!(
2091 snapshot
2092 .buffer_ids_for_range(range.clone())
2093 .collect::<Vec<_>>(),
2094 buffer_ids,
2095 "buffer_ids_for_range({range:?}"
2096 );
2097 }
2098
2099 assert_position_translation(&snapshot);
2100 assert_line_indents(&snapshot);
2101
2102 assert_eq!(
2103 snapshot
2104 .diff_hunks_in_range(0..snapshot.len())
2105 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
2106 .collect::<Vec<_>>(),
2107 &[0..1, 2..4, 5..7, 9..10, 12..13, 14..17]
2108 );
2109
2110 buffer_2.update(cx, |buffer, cx| {
2111 buffer.edit_via_marked_text(
2112 indoc!(
2113 "
2114 eight
2115 «»eleven
2116 THIRTEEN
2117 FOURTEEN
2118 "
2119 ),
2120 None,
2121 cx,
2122 );
2123 });
2124
2125 assert_new_snapshot(
2126 &multibuffer,
2127 &mut snapshot,
2128 &mut subscription,
2129 cx,
2130 indoc!(
2131 "
2132 + ZERO
2133 one
2134 - two
2135 + TWO
2136 three
2137 - four
2138 - five
2139 six
2140
2141 - seven
2142 eight
2143 eleven
2144 - twelve
2145 + THIRTEEN
2146 + FOURTEEN
2147 "
2148 ),
2149 );
2150
2151 assert_line_indents(&snapshot);
2152}
2153
2154/// A naive implementation of a multi-buffer that does not maintain
2155/// any derived state, used for comparison in a randomized test.
2156#[derive(Default)]
2157struct ReferenceMultibuffer {
2158 excerpts: Vec<ReferenceExcerpt>,
2159 diffs: HashMap<BufferId, Entity<BufferDiff>>,
2160}
2161
2162#[derive(Debug)]
2163struct ReferenceExcerpt {
2164 id: ExcerptId,
2165 buffer: Entity<Buffer>,
2166 range: Range<text::Anchor>,
2167 expanded_diff_hunks: Vec<text::Anchor>,
2168}
2169
2170#[derive(Debug)]
2171struct ReferenceRegion {
2172 buffer_id: Option<BufferId>,
2173 range: Range<usize>,
2174 buffer_start: Option<Point>,
2175 status: Option<DiffHunkStatus>,
2176 excerpt_id: Option<ExcerptId>,
2177}
2178
2179impl ReferenceMultibuffer {
2180 fn expand_excerpts(&mut self, excerpts: &HashSet<ExcerptId>, line_count: u32, cx: &App) {
2181 if line_count == 0 {
2182 return;
2183 }
2184
2185 for id in excerpts {
2186 let excerpt = self.excerpts.iter_mut().find(|e| e.id == *id).unwrap();
2187 let snapshot = excerpt.buffer.read(cx).snapshot();
2188 let mut point_range = excerpt.range.to_point(&snapshot);
2189 point_range.start = Point::new(point_range.start.row.saturating_sub(line_count), 0);
2190 point_range.end =
2191 snapshot.clip_point(Point::new(point_range.end.row + line_count, 0), Bias::Left);
2192 point_range.end.column = snapshot.line_len(point_range.end.row);
2193 excerpt.range =
2194 snapshot.anchor_before(point_range.start)..snapshot.anchor_after(point_range.end);
2195 }
2196 }
2197
2198 fn remove_excerpt(&mut self, id: ExcerptId, cx: &App) {
2199 let ix = self
2200 .excerpts
2201 .iter()
2202 .position(|excerpt| excerpt.id == id)
2203 .unwrap();
2204 let excerpt = self.excerpts.remove(ix);
2205 let buffer = excerpt.buffer.read(cx);
2206 let id = buffer.remote_id();
2207 log::info!(
2208 "Removing excerpt {}: {:?}",
2209 ix,
2210 buffer
2211 .text_for_range(excerpt.range.to_offset(buffer))
2212 .collect::<String>(),
2213 );
2214 if !self
2215 .excerpts
2216 .iter()
2217 .any(|excerpt| excerpt.buffer.read(cx).remote_id() == id)
2218 {
2219 self.diffs.remove(&id);
2220 }
2221 }
2222
2223 fn insert_excerpt_after(
2224 &mut self,
2225 prev_id: ExcerptId,
2226 new_excerpt_id: ExcerptId,
2227 (buffer_handle, anchor_range): (Entity<Buffer>, Range<text::Anchor>),
2228 ) {
2229 let excerpt_ix = if prev_id == ExcerptId::max() {
2230 self.excerpts.len()
2231 } else {
2232 self.excerpts
2233 .iter()
2234 .position(|excerpt| excerpt.id == prev_id)
2235 .unwrap()
2236 + 1
2237 };
2238 self.excerpts.insert(
2239 excerpt_ix,
2240 ReferenceExcerpt {
2241 id: new_excerpt_id,
2242 buffer: buffer_handle,
2243 range: anchor_range,
2244 expanded_diff_hunks: Vec::new(),
2245 },
2246 );
2247 }
2248
2249 fn expand_diff_hunks(&mut self, excerpt_id: ExcerptId, range: Range<text::Anchor>, cx: &App) {
2250 let excerpt = self
2251 .excerpts
2252 .iter_mut()
2253 .find(|e| e.id == excerpt_id)
2254 .unwrap();
2255 let buffer = excerpt.buffer.read(cx).snapshot();
2256 let buffer_id = buffer.remote_id();
2257 let Some(diff) = self.diffs.get(&buffer_id) else {
2258 return;
2259 };
2260 let excerpt_range = excerpt.range.to_offset(&buffer);
2261 for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) {
2262 let hunk_range = hunk.buffer_range.to_offset(&buffer);
2263 if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end {
2264 continue;
2265 }
2266 if let Err(ix) = excerpt
2267 .expanded_diff_hunks
2268 .binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer))
2269 {
2270 log::info!(
2271 "expanding diff hunk {:?}. excerpt:{:?}, excerpt range:{:?}",
2272 hunk_range,
2273 excerpt_id,
2274 excerpt_range
2275 );
2276 excerpt
2277 .expanded_diff_hunks
2278 .insert(ix, hunk.buffer_range.start);
2279 } else {
2280 log::trace!("hunk {hunk_range:?} already expanded in excerpt {excerpt_id:?}");
2281 }
2282 }
2283 }
2284
2285 fn expected_content(&self, cx: &App) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
2286 let mut text = String::new();
2287 let mut regions = Vec::<ReferenceRegion>::new();
2288 let mut excerpt_boundary_rows = HashSet::default();
2289 for excerpt in &self.excerpts {
2290 excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32));
2291 let buffer = excerpt.buffer.read(cx);
2292 let buffer_range = excerpt.range.to_offset(buffer);
2293 let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx);
2294 let base_buffer = diff.base_text();
2295
2296 let mut offset = buffer_range.start;
2297 let mut hunks = diff
2298 .hunks_intersecting_range(excerpt.range.clone(), buffer, cx)
2299 .peekable();
2300
2301 while let Some(hunk) = hunks.next() {
2302 // Ignore hunks that are outside the excerpt range.
2303 let mut hunk_range = hunk.buffer_range.to_offset(buffer);
2304
2305 hunk_range.end = hunk_range.end.min(buffer_range.end);
2306 if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start {
2307 log::trace!("skipping hunk outside excerpt range");
2308 continue;
2309 }
2310
2311 if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| {
2312 expanded_anchor.to_offset(&buffer).max(buffer_range.start)
2313 == hunk_range.start.max(buffer_range.start)
2314 }) {
2315 log::trace!("skipping a hunk that's not marked as expanded");
2316 continue;
2317 }
2318
2319 if !hunk.buffer_range.start.is_valid(&buffer) {
2320 log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
2321 continue;
2322 }
2323
2324 if hunk_range.start >= offset {
2325 // Add the buffer text before the hunk
2326 let len = text.len();
2327 text.extend(buffer.text_for_range(offset..hunk_range.start));
2328 regions.push(ReferenceRegion {
2329 buffer_id: Some(buffer.remote_id()),
2330 range: len..text.len(),
2331 buffer_start: Some(buffer.offset_to_point(offset)),
2332 status: None,
2333 excerpt_id: Some(excerpt.id),
2334 });
2335
2336 // Add the deleted text for the hunk.
2337 if !hunk.diff_base_byte_range.is_empty() {
2338 let mut base_text = base_buffer
2339 .text_for_range(hunk.diff_base_byte_range.clone())
2340 .collect::<String>();
2341 if !base_text.ends_with('\n') {
2342 base_text.push('\n');
2343 }
2344 let len = text.len();
2345 text.push_str(&base_text);
2346 regions.push(ReferenceRegion {
2347 buffer_id: Some(base_buffer.remote_id()),
2348 range: len..text.len(),
2349 buffer_start: Some(
2350 base_buffer.offset_to_point(hunk.diff_base_byte_range.start),
2351 ),
2352 status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
2353 excerpt_id: Some(excerpt.id),
2354 });
2355 }
2356
2357 offset = hunk_range.start;
2358 }
2359
2360 // Add the inserted text for the hunk.
2361 if hunk_range.end > offset {
2362 let len = text.len();
2363 text.extend(buffer.text_for_range(offset..hunk_range.end));
2364 regions.push(ReferenceRegion {
2365 buffer_id: Some(buffer.remote_id()),
2366 range: len..text.len(),
2367 buffer_start: Some(buffer.offset_to_point(offset)),
2368 status: Some(DiffHunkStatus::added(hunk.secondary_status)),
2369 excerpt_id: Some(excerpt.id),
2370 });
2371 offset = hunk_range.end;
2372 }
2373 }
2374
2375 // Add the buffer text for the rest of the excerpt.
2376 let len = text.len();
2377 text.extend(buffer.text_for_range(offset..buffer_range.end));
2378 text.push('\n');
2379 regions.push(ReferenceRegion {
2380 buffer_id: Some(buffer.remote_id()),
2381 range: len..text.len(),
2382 buffer_start: Some(buffer.offset_to_point(offset)),
2383 status: None,
2384 excerpt_id: Some(excerpt.id),
2385 });
2386 }
2387
2388 // Remove final trailing newline.
2389 if self.excerpts.is_empty() {
2390 regions.push(ReferenceRegion {
2391 buffer_id: None,
2392 range: 0..1,
2393 buffer_start: Some(Point::new(0, 0)),
2394 status: None,
2395 excerpt_id: None,
2396 });
2397 } else {
2398 text.pop();
2399 }
2400
2401 // Retrieve the row info using the region that contains
2402 // the start of each multi-buffer line.
2403 let mut ix = 0;
2404 let row_infos = text
2405 .split('\n')
2406 .map(|line| {
2407 let row_info = regions
2408 .iter()
2409 .position(|region| region.range.contains(&ix))
2410 .map_or(RowInfo::default(), |region_ix| {
2411 let region = ®ions[region_ix];
2412 let buffer_row = region.buffer_start.map(|start_point| {
2413 start_point.row
2414 + text[region.range.start..ix].matches('\n').count() as u32
2415 });
2416 let is_excerpt_start = region_ix == 0
2417 || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id
2418 || regions[region_ix - 1].range.is_empty();
2419 let mut is_excerpt_end = region_ix == regions.len() - 1
2420 || ®ions[region_ix + 1].excerpt_id != ®ion.excerpt_id;
2421 let is_start = !text[region.range.start..ix].contains('\n');
2422 let mut is_end = if region.range.end > text.len() {
2423 !text[ix..].contains('\n')
2424 } else {
2425 text[ix..region.range.end.min(text.len())]
2426 .matches('\n')
2427 .count()
2428 == 1
2429 };
2430 if region_ix < regions.len() - 1
2431 && !text[ix..].contains("\n")
2432 && region.status == Some(DiffHunkStatus::added_none())
2433 && regions[region_ix + 1].excerpt_id == region.excerpt_id
2434 && regions[region_ix + 1].range.start == text.len()
2435 {
2436 is_end = true;
2437 is_excerpt_end = true;
2438 }
2439 let mut expand_direction = None;
2440 if let Some(buffer) = &self
2441 .excerpts
2442 .iter()
2443 .find(|e| e.id == region.excerpt_id.unwrap())
2444 .map(|e| e.buffer.clone())
2445 {
2446 let needs_expand_up =
2447 is_excerpt_start && is_start && buffer_row.unwrap() > 0;
2448 let needs_expand_down = is_excerpt_end
2449 && is_end
2450 && buffer.read(cx).max_point().row > buffer_row.unwrap();
2451 expand_direction = if needs_expand_up && needs_expand_down {
2452 Some(ExpandExcerptDirection::UpAndDown)
2453 } else if needs_expand_up {
2454 Some(ExpandExcerptDirection::Up)
2455 } else if needs_expand_down {
2456 Some(ExpandExcerptDirection::Down)
2457 } else {
2458 None
2459 };
2460 }
2461 RowInfo {
2462 buffer_id: region.buffer_id,
2463 diff_status: region.status,
2464 buffer_row,
2465 multibuffer_row: Some(MultiBufferRow(
2466 text[..ix].matches('\n').count() as u32
2467 )),
2468 expand_info: expand_direction.zip(region.excerpt_id).map(
2469 |(direction, excerpt_id)| ExpandInfo {
2470 direction,
2471 excerpt_id,
2472 },
2473 ),
2474 }
2475 });
2476 ix += line.len() + 1;
2477 row_info
2478 })
2479 .collect();
2480
2481 (text, row_infos, excerpt_boundary_rows)
2482 }
2483
2484 fn diffs_updated(&mut self, cx: &App) {
2485 for excerpt in &mut self.excerpts {
2486 let buffer = excerpt.buffer.read(cx).snapshot();
2487 let excerpt_range = excerpt.range.to_offset(&buffer);
2488 let buffer_id = buffer.remote_id();
2489 let diff = self.diffs.get(&buffer_id).unwrap().read(cx);
2490 let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable();
2491 excerpt.expanded_diff_hunks.retain(|hunk_anchor| {
2492 if !hunk_anchor.is_valid(&buffer) {
2493 return false;
2494 }
2495 while let Some(hunk) = hunks.peek() {
2496 match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) {
2497 cmp::Ordering::Less => {
2498 hunks.next();
2499 }
2500 cmp::Ordering::Equal => {
2501 let hunk_range = hunk.buffer_range.to_offset(&buffer);
2502 return hunk_range.end >= excerpt_range.start
2503 && hunk_range.start <= excerpt_range.end;
2504 }
2505 cmp::Ordering::Greater => break,
2506 }
2507 }
2508 false
2509 });
2510 }
2511 }
2512
2513 fn add_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut App) {
2514 let buffer_id = diff.read(cx).buffer_id;
2515 self.diffs.insert(buffer_id, diff);
2516 }
2517}
2518
2519#[gpui::test(iterations = 100)]
2520async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
2521 let operations = env::var("OPERATIONS")
2522 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2523 .unwrap_or(10);
2524
2525 let mut buffers: Vec<Entity<Buffer>> = Vec::new();
2526 let mut base_texts: HashMap<BufferId, String> = HashMap::default();
2527 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2528 let mut reference = ReferenceMultibuffer::default();
2529 let mut anchors = Vec::new();
2530 let mut old_versions = Vec::new();
2531 let mut needs_diff_calculation = false;
2532
2533 for _ in 0..operations {
2534 match rng.gen_range(0..100) {
2535 0..=14 if !buffers.is_empty() => {
2536 let buffer = buffers.choose(&mut rng).unwrap();
2537 buffer.update(cx, |buf, cx| {
2538 let edit_count = rng.gen_range(1..5);
2539 buf.randomly_edit(&mut rng, edit_count, cx);
2540 log::info!("buffer text:\n{}", buf.text());
2541 needs_diff_calculation = true;
2542 });
2543 cx.update(|cx| reference.diffs_updated(cx));
2544 }
2545 15..=19 if !reference.excerpts.is_empty() => {
2546 multibuffer.update(cx, |multibuffer, cx| {
2547 let ids = multibuffer.excerpt_ids();
2548 let mut excerpts = HashSet::default();
2549 for _ in 0..rng.gen_range(0..ids.len()) {
2550 excerpts.extend(ids.choose(&mut rng).copied());
2551 }
2552
2553 let line_count = rng.gen_range(0..5);
2554
2555 let excerpt_ixs = excerpts
2556 .iter()
2557 .map(|id| reference.excerpts.iter().position(|e| e.id == *id).unwrap())
2558 .collect::<Vec<_>>();
2559 log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
2560 multibuffer.expand_excerpts(
2561 excerpts.iter().cloned(),
2562 line_count,
2563 ExpandExcerptDirection::UpAndDown,
2564 cx,
2565 );
2566
2567 reference.expand_excerpts(&excerpts, line_count, cx);
2568 });
2569 }
2570 20..=29 if !reference.excerpts.is_empty() => {
2571 let mut ids_to_remove = vec![];
2572 for _ in 0..rng.gen_range(1..=3) {
2573 let Some(excerpt) = reference.excerpts.choose(&mut rng) else {
2574 break;
2575 };
2576 let id = excerpt.id;
2577 cx.update(|cx| reference.remove_excerpt(id, cx));
2578 ids_to_remove.push(id);
2579 }
2580 let snapshot =
2581 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2582 ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
2583 drop(snapshot);
2584 multibuffer.update(cx, |multibuffer, cx| {
2585 multibuffer.remove_excerpts(ids_to_remove, cx)
2586 });
2587 }
2588 30..=39 if !reference.excerpts.is_empty() => {
2589 let multibuffer =
2590 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2591 let offset =
2592 multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left);
2593 let bias = if rng.gen() { Bias::Left } else { Bias::Right };
2594 log::info!("Creating anchor at {} with bias {:?}", offset, bias);
2595 anchors.push(multibuffer.anchor_at(offset, bias));
2596 anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
2597 }
2598 40..=44 if !anchors.is_empty() => {
2599 let multibuffer =
2600 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2601 let prev_len = anchors.len();
2602 anchors = multibuffer
2603 .refresh_anchors(&anchors)
2604 .into_iter()
2605 .map(|a| a.1)
2606 .collect();
2607
2608 // Ensure the newly-refreshed anchors point to a valid excerpt and don't
2609 // overshoot its boundaries.
2610 assert_eq!(anchors.len(), prev_len);
2611 for anchor in &anchors {
2612 if anchor.excerpt_id == ExcerptId::min()
2613 || anchor.excerpt_id == ExcerptId::max()
2614 {
2615 continue;
2616 }
2617
2618 let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
2619 assert_eq!(excerpt.id, anchor.excerpt_id);
2620 assert!(excerpt.contains(anchor));
2621 }
2622 }
2623 45..=55 if !reference.excerpts.is_empty() => {
2624 multibuffer.update(cx, |multibuffer, cx| {
2625 let snapshot = multibuffer.snapshot(cx);
2626 let excerpt_ix = rng.gen_range(0..reference.excerpts.len());
2627 let excerpt = &reference.excerpts[excerpt_ix];
2628 let start = excerpt.range.start;
2629 let end = excerpt.range.end;
2630 let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap()
2631 ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap();
2632
2633 log::info!(
2634 "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})",
2635 range.to_offset(&snapshot),
2636 excerpt.id,
2637 excerpt.buffer.read(cx).remote_id(),
2638 );
2639 reference.expand_diff_hunks(excerpt.id, start..end, cx);
2640 multibuffer.expand_diff_hunks(vec![range], cx);
2641 });
2642 }
2643 56..=85 if needs_diff_calculation => {
2644 multibuffer.update(cx, |multibuffer, cx| {
2645 for buffer in multibuffer.all_buffers() {
2646 let snapshot = buffer.read(cx).snapshot();
2647 multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
2648 cx,
2649 |diff, cx| {
2650 log::info!(
2651 "recalculating diff for buffer {:?}",
2652 snapshot.remote_id(),
2653 );
2654 diff.recalculate_diff_sync(snapshot.text, cx);
2655 },
2656 );
2657 }
2658 reference.diffs_updated(cx);
2659 needs_diff_calculation = false;
2660 });
2661 }
2662 _ => {
2663 let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
2664 let mut base_text = util::RandomCharIter::new(&mut rng)
2665 .take(256)
2666 .collect::<String>();
2667
2668 let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
2669 text::LineEnding::normalize(&mut base_text);
2670 base_texts.insert(
2671 buffer.read_with(cx, |buffer, _| buffer.remote_id()),
2672 base_text,
2673 );
2674 buffers.push(buffer);
2675 buffers.last().unwrap()
2676 } else {
2677 buffers.choose(&mut rng).unwrap()
2678 };
2679
2680 let prev_excerpt_ix = rng.gen_range(0..=reference.excerpts.len());
2681 let prev_excerpt_id = reference
2682 .excerpts
2683 .get(prev_excerpt_ix)
2684 .map_or(ExcerptId::max(), |e| e.id);
2685 let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len());
2686
2687 let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| {
2688 let end_row = rng.gen_range(0..=buffer.max_point().row);
2689 let start_row = rng.gen_range(0..=end_row);
2690 let end_ix = buffer.point_to_offset(Point::new(end_row, 0));
2691 let start_ix = buffer.point_to_offset(Point::new(start_row, 0));
2692 let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
2693
2694 log::info!(
2695 "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
2696 excerpt_ix,
2697 reference.excerpts.len(),
2698 buffer.remote_id(),
2699 buffer.text(),
2700 start_ix..end_ix,
2701 &buffer.text()[start_ix..end_ix]
2702 );
2703
2704 (start_ix..end_ix, anchor_range)
2705 });
2706
2707 multibuffer.update(cx, |multibuffer, cx| {
2708 let id = buffer_handle.read(cx).remote_id();
2709 if multibuffer.diff_for(id).is_none() {
2710 let base_text = base_texts.get(&id).unwrap();
2711 let diff = cx.new(|cx| {
2712 BufferDiff::new_with_base_text(base_text, &buffer_handle, cx)
2713 });
2714 reference.add_diff(diff.clone(), cx);
2715 multibuffer.add_diff(diff, cx)
2716 }
2717 });
2718
2719 let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
2720 multibuffer
2721 .insert_excerpts_after(
2722 prev_excerpt_id,
2723 buffer_handle.clone(),
2724 [ExcerptRange {
2725 context: range,
2726 primary: None,
2727 }],
2728 cx,
2729 )
2730 .pop()
2731 .unwrap()
2732 });
2733
2734 reference.insert_excerpt_after(
2735 prev_excerpt_id,
2736 excerpt_id,
2737 (buffer_handle.clone(), anchor_range),
2738 );
2739 }
2740 }
2741
2742 if rng.gen_bool(0.3) {
2743 multibuffer.update(cx, |multibuffer, cx| {
2744 old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
2745 })
2746 }
2747
2748 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2749 let actual_text = snapshot.text();
2750 let actual_boundary_rows = snapshot
2751 .excerpt_boundaries_in_range(0..)
2752 .map(|b| b.row)
2753 .collect::<HashSet<_>>();
2754 let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
2755
2756 let (expected_text, expected_row_infos, expected_boundary_rows) =
2757 cx.update(|cx| reference.expected_content(cx));
2758
2759 let has_diff = actual_row_infos
2760 .iter()
2761 .any(|info| info.diff_status.is_some())
2762 || expected_row_infos
2763 .iter()
2764 .any(|info| info.diff_status.is_some());
2765 let actual_diff = format_diff(
2766 &actual_text,
2767 &actual_row_infos,
2768 &actual_boundary_rows,
2769 Some(has_diff),
2770 );
2771 let expected_diff = format_diff(
2772 &expected_text,
2773 &expected_row_infos,
2774 &expected_boundary_rows,
2775 Some(has_diff),
2776 );
2777
2778 log::info!("Multibuffer content:\n{}", actual_diff);
2779
2780 assert_eq!(
2781 actual_row_infos.len(),
2782 actual_text.split('\n').count(),
2783 "line count: {}",
2784 actual_text.split('\n').count()
2785 );
2786 pretty_assertions::assert_eq!(actual_diff, expected_diff);
2787 pretty_assertions::assert_eq!(actual_text, expected_text);
2788 pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
2789
2790 for _ in 0..5 {
2791 let start_row = rng.gen_range(0..=expected_row_infos.len());
2792 assert_eq!(
2793 snapshot
2794 .row_infos(MultiBufferRow(start_row as u32))
2795 .collect::<Vec<_>>(),
2796 &expected_row_infos[start_row..],
2797 "buffer_rows({})",
2798 start_row
2799 );
2800 }
2801
2802 assert_eq!(
2803 snapshot.widest_line_number(),
2804 expected_row_infos
2805 .into_iter()
2806 .filter_map(|info| {
2807 if info.diff_status.is_some_and(|status| status.is_deleted()) {
2808 None
2809 } else {
2810 info.buffer_row
2811 }
2812 })
2813 .max()
2814 .unwrap()
2815 + 1
2816 );
2817
2818 assert_consistent_line_numbers(&snapshot);
2819 assert_position_translation(&snapshot);
2820
2821 for (row, line) in expected_text.split('\n').enumerate() {
2822 assert_eq!(
2823 snapshot.line_len(MultiBufferRow(row as u32)),
2824 line.len() as u32,
2825 "line_len({}).",
2826 row
2827 );
2828 }
2829
2830 let text_rope = Rope::from(expected_text.as_str());
2831 for _ in 0..10 {
2832 let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
2833 let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
2834
2835 let text_for_range = snapshot
2836 .text_for_range(start_ix..end_ix)
2837 .collect::<String>();
2838 assert_eq!(
2839 text_for_range,
2840 &expected_text[start_ix..end_ix],
2841 "incorrect text for range {:?}",
2842 start_ix..end_ix
2843 );
2844
2845 let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
2846 assert_eq!(
2847 snapshot.text_summary_for_range::<TextSummary, _>(start_ix..end_ix),
2848 expected_summary,
2849 "incorrect summary for range {:?}",
2850 start_ix..end_ix
2851 );
2852 }
2853
2854 // Anchor resolution
2855 let summaries = snapshot.summaries_for_anchors::<usize, _>(&anchors);
2856 assert_eq!(anchors.len(), summaries.len());
2857 for (anchor, resolved_offset) in anchors.iter().zip(summaries) {
2858 assert!(resolved_offset <= snapshot.len());
2859 assert_eq!(
2860 snapshot.summary_for_anchor::<usize>(anchor),
2861 resolved_offset,
2862 "anchor: {:?}",
2863 anchor
2864 );
2865 }
2866
2867 for _ in 0..10 {
2868 let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
2869 assert_eq!(
2870 snapshot.reversed_chars_at(end_ix).collect::<String>(),
2871 expected_text[..end_ix].chars().rev().collect::<String>(),
2872 );
2873 }
2874
2875 for _ in 0..10 {
2876 let end_ix = rng.gen_range(0..=text_rope.len());
2877 let start_ix = rng.gen_range(0..=end_ix);
2878 assert_eq!(
2879 snapshot
2880 .bytes_in_range(start_ix..end_ix)
2881 .flatten()
2882 .copied()
2883 .collect::<Vec<_>>(),
2884 expected_text.as_bytes()[start_ix..end_ix].to_vec(),
2885 "bytes_in_range({:?})",
2886 start_ix..end_ix,
2887 );
2888 }
2889 }
2890
2891 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2892 for (old_snapshot, subscription) in old_versions {
2893 let edits = subscription.consume().into_inner();
2894
2895 log::info!(
2896 "applying subscription edits to old text: {:?}: {:?}",
2897 old_snapshot.text(),
2898 edits,
2899 );
2900
2901 let mut text = old_snapshot.text();
2902 for edit in edits {
2903 let new_text: String = snapshot.text_for_range(edit.new.clone()).collect();
2904 text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text);
2905 }
2906 assert_eq!(text.to_string(), snapshot.text());
2907 }
2908}
2909
2910#[gpui::test]
2911fn test_history(cx: &mut App) {
2912 let test_settings = SettingsStore::test(cx);
2913 cx.set_global(test_settings);
2914 let group_interval: Duration = Duration::from_millis(1);
2915 let buffer_1 = cx.new(|cx| {
2916 let mut buf = Buffer::local("1234", cx);
2917 buf.set_group_interval(group_interval);
2918 buf
2919 });
2920 let buffer_2 = cx.new(|cx| {
2921 let mut buf = Buffer::local("5678", cx);
2922 buf.set_group_interval(group_interval);
2923 buf
2924 });
2925 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2926 multibuffer.update(cx, |this, _| {
2927 this.history.group_interval = group_interval;
2928 });
2929 multibuffer.update(cx, |multibuffer, cx| {
2930 multibuffer.push_excerpts(
2931 buffer_1.clone(),
2932 [ExcerptRange {
2933 context: 0..buffer_1.read(cx).len(),
2934 primary: None,
2935 }],
2936 cx,
2937 );
2938 multibuffer.push_excerpts(
2939 buffer_2.clone(),
2940 [ExcerptRange {
2941 context: 0..buffer_2.read(cx).len(),
2942 primary: None,
2943 }],
2944 cx,
2945 );
2946 });
2947
2948 let mut now = Instant::now();
2949
2950 multibuffer.update(cx, |multibuffer, cx| {
2951 let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap();
2952 multibuffer.edit(
2953 [
2954 (Point::new(0, 0)..Point::new(0, 0), "A"),
2955 (Point::new(1, 0)..Point::new(1, 0), "A"),
2956 ],
2957 None,
2958 cx,
2959 );
2960 multibuffer.edit(
2961 [
2962 (Point::new(0, 1)..Point::new(0, 1), "B"),
2963 (Point::new(1, 1)..Point::new(1, 1), "B"),
2964 ],
2965 None,
2966 cx,
2967 );
2968 multibuffer.end_transaction_at(now, cx);
2969 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
2970
2971 // Verify edited ranges for transaction 1
2972 assert_eq!(
2973 multibuffer.edited_ranges_for_transaction(transaction_1, cx),
2974 &[
2975 Point::new(0, 0)..Point::new(0, 2),
2976 Point::new(1, 0)..Point::new(1, 2)
2977 ]
2978 );
2979
2980 // Edit buffer 1 through the multibuffer
2981 now += 2 * group_interval;
2982 multibuffer.start_transaction_at(now, cx);
2983 multibuffer.edit([(2..2, "C")], None, cx);
2984 multibuffer.end_transaction_at(now, cx);
2985 assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678");
2986
2987 // Edit buffer 1 independently
2988 buffer_1.update(cx, |buffer_1, cx| {
2989 buffer_1.start_transaction_at(now);
2990 buffer_1.edit([(3..3, "D")], None, cx);
2991 buffer_1.end_transaction_at(now, cx);
2992
2993 now += 2 * group_interval;
2994 buffer_1.start_transaction_at(now);
2995 buffer_1.edit([(4..4, "E")], None, cx);
2996 buffer_1.end_transaction_at(now, cx);
2997 });
2998 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
2999
3000 // An undo in the multibuffer undoes the multibuffer transaction
3001 // and also any individual buffer edits that have occurred since
3002 // that transaction.
3003 multibuffer.undo(cx);
3004 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3005
3006 multibuffer.undo(cx);
3007 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3008
3009 multibuffer.redo(cx);
3010 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3011
3012 multibuffer.redo(cx);
3013 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
3014
3015 // Undo buffer 2 independently.
3016 buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx));
3017 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678");
3018
3019 // An undo in the multibuffer undoes the components of the
3020 // the last multibuffer transaction that are not already undone.
3021 multibuffer.undo(cx);
3022 assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678");
3023
3024 multibuffer.undo(cx);
3025 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3026
3027 multibuffer.redo(cx);
3028 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3029
3030 buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx));
3031 assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
3032
3033 // Redo stack gets cleared after an edit.
3034 now += 2 * group_interval;
3035 multibuffer.start_transaction_at(now, cx);
3036 multibuffer.edit([(0..0, "X")], None, cx);
3037 multibuffer.end_transaction_at(now, cx);
3038 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3039 multibuffer.redo(cx);
3040 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3041 multibuffer.undo(cx);
3042 assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
3043 multibuffer.undo(cx);
3044 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3045
3046 // Transactions can be grouped manually.
3047 multibuffer.redo(cx);
3048 multibuffer.redo(cx);
3049 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3050 multibuffer.group_until_transaction(transaction_1, cx);
3051 multibuffer.undo(cx);
3052 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3053 multibuffer.redo(cx);
3054 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3055 });
3056}
3057
3058#[gpui::test]
3059async fn test_enclosing_indent(cx: &mut TestAppContext) {
3060 async fn enclosing_indent(
3061 text: &str,
3062 buffer_row: u32,
3063 cx: &mut TestAppContext,
3064 ) -> Option<(Range<u32>, LineIndent)> {
3065 let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
3066 let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx));
3067 let (range, indent) = snapshot
3068 .enclosing_indent(MultiBufferRow(buffer_row))
3069 .await?;
3070 Some((range.start.0..range.end.0, indent))
3071 }
3072
3073 assert_eq!(
3074 enclosing_indent(
3075 indoc!(
3076 "
3077 fn b() {
3078 if c {
3079 let d = 2;
3080 }
3081 }
3082 "
3083 ),
3084 1,
3085 cx,
3086 )
3087 .await,
3088 Some((
3089 1..2,
3090 LineIndent {
3091 tabs: 0,
3092 spaces: 4,
3093 line_blank: false,
3094 }
3095 ))
3096 );
3097
3098 assert_eq!(
3099 enclosing_indent(
3100 indoc!(
3101 "
3102 fn b() {
3103 if c {
3104 let d = 2;
3105 }
3106 }
3107 "
3108 ),
3109 2,
3110 cx,
3111 )
3112 .await,
3113 Some((
3114 1..2,
3115 LineIndent {
3116 tabs: 0,
3117 spaces: 4,
3118 line_blank: false,
3119 }
3120 ))
3121 );
3122
3123 assert_eq!(
3124 enclosing_indent(
3125 indoc!(
3126 "
3127 fn b() {
3128 if c {
3129 let d = 2;
3130
3131 let e = 5;
3132 }
3133 }
3134 "
3135 ),
3136 3,
3137 cx,
3138 )
3139 .await,
3140 Some((
3141 1..4,
3142 LineIndent {
3143 tabs: 0,
3144 spaces: 4,
3145 line_blank: false,
3146 }
3147 ))
3148 );
3149}
3150
3151#[gpui::test]
3152fn test_summaries_for_anchors(cx: &mut TestAppContext) {
3153 let base_text_1 = indoc!(
3154 "
3155 bar
3156 "
3157 );
3158 let text_1 = indoc!(
3159 "
3160 BAR
3161 "
3162 );
3163 let base_text_2 = indoc!(
3164 "
3165 foo
3166 "
3167 );
3168 let text_2 = indoc!(
3169 "
3170 FOO
3171 "
3172 );
3173
3174 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
3175 let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
3176 let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx));
3177 let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx));
3178 cx.run_until_parked();
3179
3180 let mut ids = vec![];
3181 let multibuffer = cx.new(|cx| {
3182 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
3183 multibuffer.set_all_diff_hunks_expanded(cx);
3184 ids.extend(multibuffer.push_excerpts(
3185 buffer_1.clone(),
3186 [ExcerptRange {
3187 context: text::Anchor::MIN..text::Anchor::MAX,
3188 primary: None,
3189 }],
3190 cx,
3191 ));
3192 ids.extend(multibuffer.push_excerpts(
3193 buffer_2.clone(),
3194 [ExcerptRange {
3195 context: text::Anchor::MIN..text::Anchor::MAX,
3196 primary: None,
3197 }],
3198 cx,
3199 ));
3200 multibuffer.add_diff(diff_1.clone(), cx);
3201 multibuffer.add_diff(diff_2.clone(), cx);
3202 multibuffer
3203 });
3204
3205 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
3206 (multibuffer.snapshot(cx), multibuffer.subscribe())
3207 });
3208
3209 assert_new_snapshot(
3210 &multibuffer,
3211 &mut snapshot,
3212 &mut subscription,
3213 cx,
3214 indoc!(
3215 "
3216 - bar
3217 + BAR
3218
3219 - foo
3220 + FOO
3221 "
3222 ),
3223 );
3224
3225 let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
3226 let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
3227
3228 let anchor_1 = Anchor::in_buffer(ids[0], id_1, text::Anchor::MIN);
3229 let point_1 = snapshot.summaries_for_anchors::<Point, _>([&anchor_1])[0];
3230 assert_eq!(point_1, Point::new(0, 0));
3231
3232 let anchor_2 = Anchor::in_buffer(ids[1], id_2, text::Anchor::MIN);
3233 let point_2 = snapshot.summaries_for_anchors::<Point, _>([&anchor_2])[0];
3234 assert_eq!(point_2, Point::new(3, 0));
3235}
3236
3237#[gpui::test]
3238fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
3239 let base_text_1 = "one\ntwo".to_owned();
3240 let text_1 = "one\n".to_owned();
3241
3242 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
3243 let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
3244 cx.run_until_parked();
3245
3246 let multibuffer = cx.new(|cx| {
3247 let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
3248 multibuffer.add_diff(diff_1.clone(), cx);
3249 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
3250 multibuffer
3251 });
3252
3253 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
3254 (multibuffer.snapshot(cx), multibuffer.subscribe())
3255 });
3256
3257 assert_new_snapshot(
3258 &multibuffer,
3259 &mut snapshot,
3260 &mut subscription,
3261 cx,
3262 indoc!(
3263 "
3264 one
3265 - two
3266 "
3267 ),
3268 );
3269
3270 assert_eq!(snapshot.max_point(), Point::new(2, 0));
3271 assert_eq!(snapshot.len(), 8);
3272
3273 assert_eq!(
3274 snapshot
3275 .dimensions_from_points::<Point>([Point::new(2, 0)])
3276 .collect::<Vec<_>>(),
3277 vec![Point::new(2, 0)]
3278 );
3279
3280 let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
3281 assert_eq!(translated_offset, "one\n".len());
3282 let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
3283 assert_eq!(translated_point, Point::new(1, 0));
3284
3285 // The same, for an excerpt that's not at the end of the multibuffer.
3286
3287 let text_2 = "foo\n".to_owned();
3288 let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
3289 multibuffer.update(cx, |multibuffer, cx| {
3290 multibuffer.push_excerpts(
3291 buffer_2.clone(),
3292 [ExcerptRange {
3293 context: Point::new(0, 0)..Point::new(1, 0),
3294 primary: None,
3295 }],
3296 cx,
3297 );
3298 });
3299
3300 assert_new_snapshot(
3301 &multibuffer,
3302 &mut snapshot,
3303 &mut subscription,
3304 cx,
3305 indoc!(
3306 "
3307 one
3308 - two
3309
3310 foo
3311 "
3312 ),
3313 );
3314
3315 assert_eq!(
3316 snapshot
3317 .dimensions_from_points::<Point>([Point::new(2, 0)])
3318 .collect::<Vec<_>>(),
3319 vec![Point::new(2, 0)]
3320 );
3321
3322 let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
3323 let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
3324 assert_eq!(buffer.remote_id(), buffer_1_id);
3325 assert_eq!(translated_offset, "one\n".len());
3326 let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
3327 assert_eq!(buffer.remote_id(), buffer_1_id);
3328 assert_eq!(translated_point, Point::new(1, 0));
3329}
3330
3331fn format_diff(
3332 text: &str,
3333 row_infos: &Vec<RowInfo>,
3334 boundary_rows: &HashSet<MultiBufferRow>,
3335 has_diff: Option<bool>,
3336) -> String {
3337 let has_diff =
3338 has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some()));
3339 text.split('\n')
3340 .enumerate()
3341 .zip(row_infos)
3342 .map(|((ix, line), info)| {
3343 let marker = match info.diff_status.map(|status| status.kind) {
3344 Some(DiffHunkStatusKind::Added) => "+ ",
3345 Some(DiffHunkStatusKind::Deleted) => "- ",
3346 Some(DiffHunkStatusKind::Modified) => unreachable!(),
3347 None => {
3348 if has_diff && !line.is_empty() {
3349 " "
3350 } else {
3351 ""
3352 }
3353 }
3354 };
3355 let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
3356 if has_diff {
3357 " ----------\n"
3358 } else {
3359 "---------\n"
3360 }
3361 } else {
3362 ""
3363 };
3364 format!("{boundary_row}{marker}{line}")
3365 })
3366 .collect::<Vec<_>>()
3367 .join("\n")
3368}
3369
3370#[track_caller]
3371fn assert_excerpts_match(
3372 multibuffer: &Entity<MultiBuffer>,
3373 cx: &mut TestAppContext,
3374 expected: &str,
3375) {
3376 let mut output = String::new();
3377 multibuffer.read_with(cx, |multibuffer, cx| {
3378 for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() {
3379 output.push_str("-----\n");
3380 output.extend(buffer.text_for_range(range.context));
3381 if !output.ends_with('\n') {
3382 output.push('\n');
3383 }
3384 }
3385 });
3386 assert_eq!(output, expected);
3387}
3388
3389#[track_caller]
3390fn assert_new_snapshot(
3391 multibuffer: &Entity<MultiBuffer>,
3392 snapshot: &mut MultiBufferSnapshot,
3393 subscription: &mut Subscription,
3394 cx: &mut TestAppContext,
3395 expected_diff: &str,
3396) {
3397 let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
3398 let actual_text = new_snapshot.text();
3399 let line_infos = new_snapshot
3400 .row_infos(MultiBufferRow(0))
3401 .collect::<Vec<_>>();
3402 let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None);
3403 pretty_assertions::assert_eq!(actual_diff, expected_diff);
3404 check_edits(
3405 snapshot,
3406 &new_snapshot,
3407 &subscription.consume().into_inner(),
3408 );
3409 *snapshot = new_snapshot;
3410}
3411
3412#[track_caller]
3413fn check_edits(
3414 old_snapshot: &MultiBufferSnapshot,
3415 new_snapshot: &MultiBufferSnapshot,
3416 edits: &[Edit<usize>],
3417) {
3418 let mut text = old_snapshot.text();
3419 let new_text = new_snapshot.text();
3420 for edit in edits.iter().rev() {
3421 if !text.is_char_boundary(edit.old.start)
3422 || !text.is_char_boundary(edit.old.end)
3423 || !new_text.is_char_boundary(edit.new.start)
3424 || !new_text.is_char_boundary(edit.new.end)
3425 {
3426 panic!(
3427 "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}",
3428 edits, text, new_text
3429 );
3430 }
3431
3432 text.replace_range(
3433 edit.old.start..edit.old.end,
3434 &new_text[edit.new.start..edit.new.end],
3435 );
3436 }
3437
3438 pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits);
3439}
3440
3441#[track_caller]
3442fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) {
3443 let full_text = snapshot.text();
3444 for ix in 0..full_text.len() {
3445 let mut chunks = snapshot.chunks(0..snapshot.len(), false);
3446 chunks.seek(ix..snapshot.len());
3447 let tail = chunks.map(|chunk| chunk.text).collect::<String>();
3448 assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..);
3449 }
3450}
3451
3452#[track_caller]
3453fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) {
3454 let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
3455 for start_row in 1..all_line_numbers.len() {
3456 let line_numbers = snapshot
3457 .row_infos(MultiBufferRow(start_row as u32))
3458 .collect::<Vec<_>>();
3459 assert_eq!(
3460 line_numbers,
3461 all_line_numbers[start_row..],
3462 "start_row: {start_row}"
3463 );
3464 }
3465}
3466
3467#[track_caller]
3468fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
3469 let text = Rope::from(snapshot.text());
3470
3471 let mut left_anchors = Vec::new();
3472 let mut right_anchors = Vec::new();
3473 let mut offsets = Vec::new();
3474 let mut points = Vec::new();
3475 for offset in 0..=text.len() + 1 {
3476 let clipped_left = snapshot.clip_offset(offset, Bias::Left);
3477 let clipped_right = snapshot.clip_offset(offset, Bias::Right);
3478 assert_eq!(
3479 clipped_left,
3480 text.clip_offset(offset, Bias::Left),
3481 "clip_offset({offset:?}, Left)"
3482 );
3483 assert_eq!(
3484 clipped_right,
3485 text.clip_offset(offset, Bias::Right),
3486 "clip_offset({offset:?}, Right)"
3487 );
3488 assert_eq!(
3489 snapshot.offset_to_point(clipped_left),
3490 text.offset_to_point(clipped_left),
3491 "offset_to_point({clipped_left})"
3492 );
3493 assert_eq!(
3494 snapshot.offset_to_point(clipped_right),
3495 text.offset_to_point(clipped_right),
3496 "offset_to_point({clipped_right})"
3497 );
3498 let anchor_after = snapshot.anchor_after(clipped_left);
3499 assert_eq!(
3500 anchor_after.to_offset(snapshot),
3501 clipped_left,
3502 "anchor_after({clipped_left}).to_offset {anchor_after:?}"
3503 );
3504 let anchor_before = snapshot.anchor_before(clipped_left);
3505 assert_eq!(
3506 anchor_before.to_offset(snapshot),
3507 clipped_left,
3508 "anchor_before({clipped_left}).to_offset"
3509 );
3510 left_anchors.push(anchor_before);
3511 right_anchors.push(anchor_after);
3512 offsets.push(clipped_left);
3513 points.push(text.offset_to_point(clipped_left));
3514 }
3515
3516 for row in 0..text.max_point().row {
3517 for column in 0..text.line_len(row) + 1 {
3518 let point = Point { row, column };
3519 let clipped_left = snapshot.clip_point(point, Bias::Left);
3520 let clipped_right = snapshot.clip_point(point, Bias::Right);
3521 assert_eq!(
3522 clipped_left,
3523 text.clip_point(point, Bias::Left),
3524 "clip_point({point:?}, Left)"
3525 );
3526 assert_eq!(
3527 clipped_right,
3528 text.clip_point(point, Bias::Right),
3529 "clip_point({point:?}, Right)"
3530 );
3531 assert_eq!(
3532 snapshot.point_to_offset(clipped_left),
3533 text.point_to_offset(clipped_left),
3534 "point_to_offset({clipped_left:?})"
3535 );
3536 assert_eq!(
3537 snapshot.point_to_offset(clipped_right),
3538 text.point_to_offset(clipped_right),
3539 "point_to_offset({clipped_right:?})"
3540 );
3541 }
3542 }
3543
3544 assert_eq!(
3545 snapshot.summaries_for_anchors::<usize, _>(&left_anchors),
3546 offsets,
3547 "left_anchors <-> offsets"
3548 );
3549 assert_eq!(
3550 snapshot.summaries_for_anchors::<Point, _>(&left_anchors),
3551 points,
3552 "left_anchors <-> points"
3553 );
3554 assert_eq!(
3555 snapshot.summaries_for_anchors::<usize, _>(&right_anchors),
3556 offsets,
3557 "right_anchors <-> offsets"
3558 );
3559 assert_eq!(
3560 snapshot.summaries_for_anchors::<Point, _>(&right_anchors),
3561 points,
3562 "right_anchors <-> points"
3563 );
3564
3565 for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] {
3566 for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() {
3567 if ix > 0 {
3568 if *offset == 252 {
3569 if offset > &offsets[ix - 1] {
3570 let prev_anchor = left_anchors[ix - 1];
3571 assert!(
3572 anchor.cmp(&prev_anchor, snapshot).is_gt(),
3573 "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
3574 offsets[ix],
3575 offsets[ix - 1],
3576 );
3577 assert!(
3578 prev_anchor.cmp(&anchor, snapshot).is_lt(),
3579 "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
3580 offsets[ix - 1],
3581 offsets[ix],
3582 );
3583 }
3584 }
3585 }
3586 }
3587 }
3588
3589 if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
3590 assert!(offset <= buffer.len());
3591 }
3592 if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
3593 assert!(point <= buffer.max_point());
3594 }
3595}
3596
3597fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
3598 let max_row = snapshot.max_point().row;
3599 let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id();
3600 let text = text::Buffer::new(0, buffer_id, snapshot.text());
3601 let mut line_indents = text
3602 .line_indents_in_row_range(0..max_row + 1)
3603 .collect::<Vec<_>>();
3604 for start_row in 0..snapshot.max_point().row {
3605 pretty_assertions::assert_eq!(
3606 snapshot
3607 .line_indents(MultiBufferRow(start_row), |_| true)
3608 .map(|(row, indent, _)| (row.0, indent))
3609 .collect::<Vec<_>>(),
3610 &line_indents[(start_row as usize)..],
3611 "line_indents({start_row})"
3612 );
3613 }
3614
3615 line_indents.reverse();
3616 pretty_assertions::assert_eq!(
3617 snapshot
3618 .reversed_line_indents(MultiBufferRow(max_row), |_| true)
3619 .map(|(row, indent, _)| (row.0, indent))
3620 .collect::<Vec<_>>(),
3621 &line_indents[..],
3622 "reversed_line_indents({max_row})"
3623 );
3624}