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 std::time::{Duration, Instant};
11use util::RandomCharIter;
12use util::rel_path::rel_path;
13use util::test::sample_text;
14
15#[ctor::ctor]
16fn init_logger() {
17 zlog::init_test();
18}
19
20#[gpui::test]
21fn test_empty_singleton(cx: &mut App) {
22 let buffer = cx.new(|cx| Buffer::local("", cx));
23 let buffer_id = buffer.read(cx).remote_id();
24 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
25 let snapshot = multibuffer.read(cx).snapshot(cx);
26 assert_eq!(snapshot.text(), "");
27 assert_eq!(
28 snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
29 [RowInfo {
30 buffer_id: Some(buffer_id),
31 buffer_row: Some(0),
32 multibuffer_row: Some(MultiBufferRow(0)),
33 diff_status: None,
34 expand_info: None,
35 wrapped_buffer_row: None,
36 }]
37 );
38}
39
40#[gpui::test]
41fn test_singleton(cx: &mut App) {
42 let buffer = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
43 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
44
45 let snapshot = multibuffer.read(cx).snapshot(cx);
46 assert_eq!(snapshot.text(), buffer.read(cx).text());
47
48 assert_eq!(
49 snapshot
50 .row_infos(MultiBufferRow(0))
51 .map(|info| info.buffer_row)
52 .collect::<Vec<_>>(),
53 (0..buffer.read(cx).row_count())
54 .map(Some)
55 .collect::<Vec<_>>()
56 );
57 assert_consistent_line_numbers(&snapshot);
58
59 buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx));
60 let snapshot = multibuffer.read(cx).snapshot(cx);
61
62 assert_eq!(snapshot.text(), buffer.read(cx).text());
63 assert_eq!(
64 snapshot
65 .row_infos(MultiBufferRow(0))
66 .map(|info| info.buffer_row)
67 .collect::<Vec<_>>(),
68 (0..buffer.read(cx).row_count())
69 .map(Some)
70 .collect::<Vec<_>>()
71 );
72 assert_consistent_line_numbers(&snapshot);
73}
74
75#[gpui::test]
76fn test_remote(cx: &mut App) {
77 let host_buffer = cx.new(|cx| Buffer::local("a", cx));
78 let guest_buffer = cx.new(|cx| {
79 let state = host_buffer.read(cx).to_proto(cx);
80 let ops = cx
81 .foreground_executor()
82 .block_on(host_buffer.read(cx).serialize_ops(None, cx));
83 let mut buffer =
84 Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None)
85 .unwrap();
86 buffer.apply_ops(
87 ops.into_iter()
88 .map(|op| language::proto::deserialize_operation(op).unwrap()),
89 cx,
90 );
91 buffer
92 });
93 let multibuffer = cx.new(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx));
94 let snapshot = multibuffer.read(cx).snapshot(cx);
95 assert_eq!(snapshot.text(), "a");
96
97 guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx));
98 let snapshot = multibuffer.read(cx).snapshot(cx);
99 assert_eq!(snapshot.text(), "ab");
100
101 guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx));
102 let snapshot = multibuffer.read(cx).snapshot(cx);
103 assert_eq!(snapshot.text(), "abc");
104}
105
106#[gpui::test]
107fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
108 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(7, 6, 'a'), cx));
109 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(7, 6, 'g'), cx));
110 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
111
112 let events = Arc::new(RwLock::new(Vec::<Event>::new()));
113 multibuffer.update(cx, |_, cx| {
114 let events = events.clone();
115 cx.subscribe(&multibuffer, move |_, _, event, _| {
116 if let Event::Edited { .. } = event {
117 events.write().push(event.clone())
118 }
119 })
120 .detach();
121 });
122
123 let subscription = multibuffer.update(cx, |multibuffer, cx| {
124 let subscription = multibuffer.subscribe();
125 multibuffer.set_excerpt_ranges_for_path(
126 PathKey::sorted(0),
127 buffer_1.clone(),
128 &buffer_1.read(cx).snapshot(),
129 vec![ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5))],
130 cx,
131 );
132 assert_eq!(
133 subscription.consume().into_inner(),
134 [Edit {
135 old: MultiBufferOffset(0)..MultiBufferOffset(0),
136 new: MultiBufferOffset(0)..MultiBufferOffset(10)
137 }]
138 );
139
140 multibuffer.set_excerpt_ranges_for_path(
141 PathKey::sorted(0),
142 buffer_1.clone(),
143 &buffer_1.read(cx).snapshot(),
144 vec![
145 ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5)),
146 ExcerptRange::new(Point::new(5, 3)..Point::new(6, 4)),
147 ],
148 cx,
149 );
150 multibuffer.set_excerpt_ranges_for_path(
151 PathKey::sorted(1),
152 buffer_2.clone(),
153 &buffer_2.read(cx).snapshot(),
154 vec![ExcerptRange::new(Point::new(3, 1)..Point::new(3, 3))],
155 cx,
156 );
157 assert_eq!(
158 subscription.consume().into_inner(),
159 [Edit {
160 old: MultiBufferOffset(10)..MultiBufferOffset(10),
161 new: MultiBufferOffset(10)..MultiBufferOffset(22)
162 }]
163 );
164
165 subscription
166 });
167
168 // Adding excerpts emits an edited event.
169 assert_eq!(
170 events.read().as_slice(),
171 &[
172 Event::Edited {
173 edited_buffer: None,
174 is_local: true,
175 },
176 Event::Edited {
177 edited_buffer: None,
178 is_local: true,
179 },
180 Event::Edited {
181 edited_buffer: None,
182 is_local: true,
183 }
184 ]
185 );
186
187 let snapshot = multibuffer.read(cx).snapshot(cx);
188 assert_eq!(
189 snapshot.text(),
190 indoc!(
191 "
192 bbbb
193 ccccc
194 fff
195 gggg
196 jj"
197 ),
198 );
199 assert_eq!(
200 snapshot
201 .row_infos(MultiBufferRow(0))
202 .map(|info| info.buffer_row)
203 .collect::<Vec<_>>(),
204 [Some(1), Some(2), Some(5), Some(6), Some(3)]
205 );
206 assert_eq!(
207 snapshot
208 .row_infos(MultiBufferRow(2))
209 .map(|info| info.buffer_row)
210 .collect::<Vec<_>>(),
211 [Some(5), Some(6), Some(3)]
212 );
213 assert_eq!(
214 snapshot
215 .row_infos(MultiBufferRow(4))
216 .map(|info| info.buffer_row)
217 .collect::<Vec<_>>(),
218 [Some(3)]
219 );
220 assert!(
221 snapshot
222 .row_infos(MultiBufferRow(5))
223 .map(|info| info.buffer_row)
224 .collect::<Vec<_>>()
225 .is_empty()
226 );
227
228 assert_eq!(
229 boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot),
230 &[
231 (MultiBufferRow(0), "bbbb\nccccc".to_string(), true),
232 (MultiBufferRow(2), "fff\ngggg".to_string(), false),
233 (MultiBufferRow(4), "jj".to_string(), true),
234 ]
235 );
236 assert_eq!(
237 boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot),
238 &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)]
239 );
240 assert_eq!(
241 boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot),
242 &[]
243 );
244 assert_eq!(
245 boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot),
246 &[]
247 );
248 assert_eq!(
249 boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
250 &[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
251 );
252 assert_eq!(
253 boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot),
254 &[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
255 );
256 assert_eq!(
257 boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot),
258 &[(MultiBufferRow(2), "fff\ngggg".to_string(), false)]
259 );
260 assert_eq!(
261 boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot),
262 &[(MultiBufferRow(4), "jj".to_string(), true)]
263 );
264 assert_eq!(
265 boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot),
266 &[]
267 );
268
269 buffer_1.update(cx, |buffer, cx| {
270 let text = "\n";
271 buffer.edit(
272 [
273 (Point::new(0, 0)..Point::new(0, 0), text),
274 (Point::new(2, 1)..Point::new(2, 3), text),
275 ],
276 None,
277 cx,
278 );
279 });
280
281 let snapshot = multibuffer.read(cx).snapshot(cx);
282 assert_eq!(
283 snapshot.text(),
284 concat!(
285 "bbbb\n", // Preserve newlines
286 "c\n", //
287 "cc\n", //
288 "fff\n", //
289 "gggg\n", //
290 "jj" //
291 )
292 );
293
294 assert_eq!(
295 subscription.consume().into_inner(),
296 [Edit {
297 old: MultiBufferOffset(6)..MultiBufferOffset(8),
298 new: MultiBufferOffset(6)..MultiBufferOffset(7)
299 }]
300 );
301
302 let snapshot = multibuffer.read(cx).snapshot(cx);
303 assert_eq!(
304 snapshot.clip_point(Point::new(0, 5), Bias::Left),
305 Point::new(0, 4)
306 );
307 assert_eq!(
308 snapshot.clip_point(Point::new(0, 5), Bias::Right),
309 Point::new(0, 4)
310 );
311 assert_eq!(
312 snapshot.clip_point(Point::new(5, 1), Bias::Right),
313 Point::new(5, 1)
314 );
315 assert_eq!(
316 snapshot.clip_point(Point::new(5, 2), Bias::Right),
317 Point::new(5, 2)
318 );
319 assert_eq!(
320 snapshot.clip_point(Point::new(5, 3), Bias::Right),
321 Point::new(5, 2)
322 );
323
324 let snapshot = multibuffer.update(cx, |multibuffer, cx| {
325 multibuffer.remove_excerpts_for_path(PathKey::sorted(1), cx);
326 multibuffer.snapshot(cx)
327 });
328
329 assert_eq!(
330 snapshot.text(),
331 concat!(
332 "bbbb\n", // Preserve newlines
333 "c\n", //
334 "cc\n", //
335 "fff\n", //
336 "gggg", //
337 )
338 );
339
340 fn boundaries_in_range(
341 range: Range<Point>,
342 snapshot: &MultiBufferSnapshot,
343 ) -> Vec<(MultiBufferRow, String, bool)> {
344 snapshot
345 .excerpt_boundaries_in_range(range)
346 .map(|boundary| {
347 let starts_new_buffer = boundary.starts_new_buffer();
348 (
349 boundary.row,
350 boundary
351 .next
352 .buffer
353 .text_for_range(boundary.next.range.context)
354 .collect::<String>(),
355 starts_new_buffer,
356 )
357 })
358 .collect::<Vec<_>>()
359 }
360}
361
362#[gpui::test]
363async fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
364 let base_text = "one\ntwo\nthree\n";
365 let text = "one\nthree\n";
366 let buffer = cx.new(|cx| Buffer::local(text, cx));
367 let diff = cx
368 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
369 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
370 multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx));
371
372 let (before, after) = multibuffer.update(cx, |multibuffer, cx| {
373 let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0));
374 let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0));
375 multibuffer.set_all_diff_hunks_expanded(cx);
376 (before, after)
377 });
378 cx.run_until_parked();
379
380 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
381 let actual_text = snapshot.text();
382 let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
383 let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None);
384 pretty_assertions::assert_eq!(
385 actual_diff,
386 indoc! {
387 " one
388 - two
389 three
390 "
391 },
392 );
393
394 multibuffer.update(cx, |multibuffer, cx| {
395 let snapshot = multibuffer.snapshot(cx);
396 assert_eq!(before.to_point(&snapshot), Point::new(1, 0));
397 assert_eq!(after.to_point(&snapshot), Point::new(2, 0));
398 assert_eq!(
399 vec![Point::new(1, 0), Point::new(2, 0),],
400 snapshot.summaries_for_anchors::<Point, _>(&[before, after]),
401 )
402 })
403}
404
405#[gpui::test]
406async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
407 let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
408 let text = "one\nfour\nseven\n";
409 let buffer = cx.new(|cx| Buffer::local(text, cx));
410 let diff = cx
411 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
412 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
413 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
414 (multibuffer.snapshot(cx), multibuffer.subscribe())
415 });
416
417 multibuffer.update(cx, |multibuffer, cx| {
418 multibuffer.add_diff(diff, cx);
419 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
420 });
421
422 assert_new_snapshot(
423 &multibuffer,
424 &mut snapshot,
425 &mut subscription,
426 cx,
427 indoc! {
428 " one
429 - two
430 - three
431 four
432 - five
433 - six
434 seven
435 - eight
436 "
437 },
438 );
439
440 assert_eq!(
441 snapshot
442 .diff_hunks_in_range(Point::new(1, 0)..Point::MAX)
443 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
444 .collect::<Vec<_>>(),
445 vec![1..3, 4..6, 7..8]
446 );
447
448 assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,);
449 assert_eq!(
450 snapshot.diff_hunk_before(Point::new(7, 0)),
451 Some(MultiBufferRow(4))
452 );
453 assert_eq!(
454 snapshot.diff_hunk_before(Point::new(4, 0)),
455 Some(MultiBufferRow(1))
456 );
457
458 multibuffer.update(cx, |multibuffer, cx| {
459 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
460 });
461
462 assert_new_snapshot(
463 &multibuffer,
464 &mut snapshot,
465 &mut subscription,
466 cx,
467 indoc! {
468 "
469 one
470 four
471 seven
472 "
473 },
474 );
475
476 assert_eq!(
477 snapshot.diff_hunk_before(Point::new(2, 0)),
478 Some(MultiBufferRow(1)),
479 );
480 assert_eq!(
481 snapshot.diff_hunk_before(Point::new(4, 0)),
482 Some(MultiBufferRow(2))
483 );
484}
485
486#[gpui::test]
487async fn test_diff_hunks_in_range_query_starting_at_added_row(cx: &mut TestAppContext) {
488 let base_text = "one\ntwo\nthree\n";
489 let text = "one\nTWO\nthree\n";
490 let buffer = cx.new(|cx| Buffer::local(text, cx));
491 let diff = cx
492 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
493 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
494 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
495 (multibuffer.snapshot(cx), multibuffer.subscribe())
496 });
497
498 multibuffer.update(cx, |multibuffer, cx| {
499 multibuffer.add_diff(diff, cx);
500 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
501 });
502
503 assert_new_snapshot(
504 &multibuffer,
505 &mut snapshot,
506 &mut subscription,
507 cx,
508 indoc! {
509 " one
510 - two
511 + TWO
512 three
513 "
514 },
515 );
516
517 assert_eq!(
518 snapshot
519 .diff_hunks_in_range(Point::new(2, 0)..Point::MAX)
520 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
521 .collect::<Vec<_>>(),
522 vec![1..3],
523 "querying starting at the added row should still return the full hunk including deleted lines"
524 );
525}
526
527#[gpui::test]
528async fn test_inverted_diff_hunks_in_range(cx: &mut TestAppContext) {
529 let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
530 let text = "ZERO\none\nTHREE\nfour\nseven\nEIGHT\nNINE\n";
531 let buffer = cx.new(|cx| Buffer::local(text, cx));
532 let diff = cx
533 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
534 let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
535 let multibuffer = cx.new(|cx| MultiBuffer::singleton(base_text_buffer.clone(), cx));
536 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
537 (multibuffer.snapshot(cx), multibuffer.subscribe())
538 });
539
540 multibuffer.update(cx, |multibuffer, cx| {
541 multibuffer.add_inverted_diff(diff, buffer.clone(), cx);
542 });
543
544 assert_new_snapshot(
545 &multibuffer,
546 &mut snapshot,
547 &mut subscription,
548 cx,
549 indoc! {
550 " one
551 - two
552 - three
553 four
554 - five
555 - six
556 seven
557 - eight
558 "
559 },
560 );
561
562 assert_eq!(
563 snapshot
564 .diff_hunks_in_range(Point::new(0, 0)..Point::MAX)
565 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
566 .collect::<Vec<_>>(),
567 vec![0..0, 1..3, 4..6, 7..8]
568 );
569
570 assert_eq!(
571 snapshot.diff_hunk_before(Point::new(1, 1)),
572 Some(MultiBufferRow(0))
573 );
574 assert_eq!(
575 snapshot.diff_hunk_before(Point::new(7, 0)),
576 Some(MultiBufferRow(4))
577 );
578 assert_eq!(
579 snapshot.diff_hunk_before(Point::new(4, 0)),
580 Some(MultiBufferRow(1))
581 );
582}
583
584#[gpui::test]
585async fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
586 let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
587 let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
588 let buffer = cx.new(|cx| Buffer::local(text, cx));
589 let diff = cx
590 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
591 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
592
593 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
594 multibuffer.add_diff(diff.clone(), cx);
595 (multibuffer.snapshot(cx), multibuffer.subscribe())
596 });
597
598 cx.executor().run_until_parked();
599 multibuffer.update(cx, |multibuffer, cx| {
600 multibuffer.set_all_diff_hunks_expanded(cx);
601 });
602
603 assert_new_snapshot(
604 &multibuffer,
605 &mut snapshot,
606 &mut subscription,
607 cx,
608 indoc! {
609 "
610 one
611 two
612 + THREE
613 four
614 five
615 - six
616 seven
617 "
618 },
619 );
620
621 // Insert a newline within an insertion hunk
622 multibuffer.update(cx, |multibuffer, cx| {
623 multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx);
624 });
625 assert_new_snapshot(
626 &multibuffer,
627 &mut snapshot,
628 &mut subscription,
629 cx,
630 indoc! {
631 "
632 one
633 two
634 + __
635 + __THREE
636 four
637 five
638 - six
639 seven
640 "
641 },
642 );
643
644 // Delete the newline before a deleted hunk.
645 multibuffer.update(cx, |multibuffer, cx| {
646 multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx);
647 });
648 assert_new_snapshot(
649 &multibuffer,
650 &mut snapshot,
651 &mut subscription,
652 cx,
653 indoc! {
654 "
655 one
656 two
657 + __
658 + __THREE
659 four
660 fiveseven
661 "
662 },
663 );
664
665 multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx));
666 assert_new_snapshot(
667 &multibuffer,
668 &mut snapshot,
669 &mut subscription,
670 cx,
671 indoc! {
672 "
673 one
674 two
675 + __
676 + __THREE
677 four
678 five
679 - six
680 seven
681 "
682 },
683 );
684
685 // Cannot (yet) insert at the beginning of a deleted hunk.
686 // (because it would put the newline in the wrong place)
687 multibuffer.update(cx, |multibuffer, cx| {
688 multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx);
689 });
690 assert_new_snapshot(
691 &multibuffer,
692 &mut snapshot,
693 &mut subscription,
694 cx,
695 indoc! {
696 "
697 one
698 two
699 + __
700 + __THREE
701 four
702 five
703 - six
704 seven
705 "
706 },
707 );
708
709 // Replace a range that ends in a deleted hunk.
710 multibuffer.update(cx, |multibuffer, cx| {
711 multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx);
712 });
713 assert_new_snapshot(
714 &multibuffer,
715 &mut snapshot,
716 &mut subscription,
717 cx,
718 indoc! {
719 "
720 one
721 two
722 + __
723 + __THREE
724 four
725 fifty-seven
726 "
727 },
728 );
729}
730
731#[gpui::test]
732fn test_excerpt_events(cx: &mut App) {
733 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'a'), cx));
734 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'm'), cx));
735
736 let leader_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
737 let follower_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
738 let follower_edit_event_count = Arc::new(RwLock::new(0));
739
740 follower_multibuffer.update(cx, |_, cx| {
741 let follower_edit_event_count = follower_edit_event_count.clone();
742 cx.subscribe(
743 &leader_multibuffer,
744 move |follower, _, event, cx| match event.clone() {
745 Event::ExcerptsAdded {
746 buffer,
747 predecessor,
748 excerpts,
749 } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
750 Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx),
751 Event::Edited { .. } => {
752 *follower_edit_event_count.write() += 1;
753 }
754 _ => {}
755 },
756 )
757 .detach();
758 });
759
760 let buffer_1_snapshot = buffer_1.read(cx).snapshot();
761 let buffer_2_snapshot = buffer_2.read(cx).snapshot();
762 leader_multibuffer.update(cx, |leader, cx| {
763 leader.set_excerpt_ranges_for_path(
764 PathKey::sorted(0),
765 buffer_1.clone(),
766 &buffer_1_snapshot,
767 vec![
768 ExcerptRange::new((0..8).to_point(&buffer_1_snapshot)),
769 ExcerptRange::new((22..26).to_point(&buffer_1_snapshot)),
770 ],
771 cx,
772 );
773 leader.set_excerpt_ranges_for_path(
774 PathKey::sorted(1),
775 buffer_2.clone(),
776 &buffer_2_snapshot,
777 vec![
778 ExcerptRange::new((0..5).to_point(&buffer_2_snapshot)),
779 ExcerptRange::new((20..25).to_point(&buffer_2_snapshot)),
780 ],
781 cx,
782 );
783 });
784 assert_eq!(
785 leader_multibuffer.read(cx).snapshot(cx).text(),
786 follower_multibuffer.read(cx).snapshot(cx).text(),
787 );
788 assert_eq!(*follower_edit_event_count.read(), 2);
789
790 leader_multibuffer.update(cx, |leader, cx| {
791 leader.set_excerpt_ranges_for_path(
792 PathKey::sorted(0),
793 buffer_1.clone(),
794 &buffer_1_snapshot,
795 vec![ExcerptRange::new((0..8).to_point(&buffer_1_snapshot))],
796 cx,
797 );
798 leader.set_excerpt_ranges_for_path(
799 PathKey::sorted(1),
800 buffer_2,
801 &buffer_2_snapshot,
802 vec![ExcerptRange::new((0..5).to_point(&buffer_2_snapshot))],
803 cx,
804 );
805 });
806 assert_eq!(
807 leader_multibuffer.read(cx).snapshot(cx).text(),
808 follower_multibuffer.read(cx).snapshot(cx).text(),
809 );
810 assert_eq!(*follower_edit_event_count.read(), 4);
811
812 leader_multibuffer.update(cx, |leader, cx| {
813 leader.clear(cx);
814 });
815 assert_eq!(
816 leader_multibuffer.read(cx).snapshot(cx).text(),
817 follower_multibuffer.read(cx).snapshot(cx).text(),
818 );
819 assert_eq!(*follower_edit_event_count.read(), 5);
820}
821
822#[gpui::test]
823fn test_expand_excerpts(cx: &mut App) {
824 let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
825 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
826
827 multibuffer.update(cx, |multibuffer, cx| {
828 multibuffer.set_excerpts_for_path(
829 PathKey::for_buffer(&buffer, cx),
830 buffer,
831 vec![
832 // Note that in this test, this first excerpt
833 // does not contain a new line
834 Point::new(3, 2)..Point::new(3, 3),
835 Point::new(7, 1)..Point::new(7, 3),
836 Point::new(15, 0)..Point::new(15, 0),
837 ],
838 1,
839 cx,
840 )
841 });
842
843 let snapshot = multibuffer.read(cx).snapshot(cx);
844
845 assert_eq!(
846 snapshot.text(),
847 concat!(
848 "ccc\n", //
849 "ddd\n", //
850 "eee", //
851 "\n", // End of excerpt
852 "ggg\n", //
853 "hhh\n", //
854 "iii", //
855 "\n", // End of excerpt
856 "ooo\n", //
857 "ppp\n", //
858 "qqq", // End of excerpt
859 )
860 );
861 drop(snapshot);
862
863 multibuffer.update(cx, |multibuffer, cx| {
864 let line_zero = multibuffer.snapshot(cx).anchor_before(Point::new(0, 0));
865 multibuffer.expand_excerpts(
866 multibuffer.excerpt_ids(),
867 1,
868 ExpandExcerptDirection::UpAndDown,
869 cx,
870 );
871 let snapshot = multibuffer.snapshot(cx);
872 let line_two = snapshot.anchor_before(Point::new(2, 0));
873 assert_eq!(line_two.cmp(&line_zero, &snapshot), cmp::Ordering::Greater);
874 });
875
876 let snapshot = multibuffer.read(cx).snapshot(cx);
877
878 assert_eq!(
879 snapshot.text(),
880 concat!(
881 "bbb\n", //
882 "ccc\n", //
883 "ddd\n", //
884 "eee\n", //
885 "fff\n", //
886 "ggg\n", //
887 "hhh\n", //
888 "iii\n", //
889 "jjj\n", // End of excerpt
890 "nnn\n", //
891 "ooo\n", //
892 "ppp\n", //
893 "qqq\n", //
894 "rrr", // End of excerpt
895 )
896 );
897}
898
899#[gpui::test(iterations = 100)]
900async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) {
901 let buffer_1 = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
902 let buffer_2 = cx.new(|cx| Buffer::local(sample_text(15, 4, 'a'), cx));
903 let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot());
904 let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot());
905 let ranges_1 = vec![
906 snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)),
907 snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)),
908 snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)),
909 ];
910 let ranges_2 = vec![
911 snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)),
912 snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)),
913 ];
914
915 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
916 let anchor_ranges_1 = multibuffer
917 .update(cx, |multibuffer, cx| {
918 multibuffer.set_anchored_excerpts_for_path(
919 PathKey::for_buffer(&buffer_1, cx),
920 buffer_1.clone(),
921 ranges_1,
922 2,
923 cx,
924 )
925 })
926 .await;
927 let snapshot_1 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
928 assert_eq!(
929 anchor_ranges_1
930 .iter()
931 .map(|range| range.to_point(&snapshot_1))
932 .collect::<Vec<_>>(),
933 vec![
934 Point::new(2, 2)..Point::new(3, 2),
935 Point::new(6, 1)..Point::new(6, 3),
936 Point::new(11, 0)..Point::new(11, 0),
937 ]
938 );
939 let anchor_ranges_2 = multibuffer
940 .update(cx, |multibuffer, cx| {
941 multibuffer.set_anchored_excerpts_for_path(
942 PathKey::for_buffer(&buffer_2, cx),
943 buffer_2.clone(),
944 ranges_2,
945 2,
946 cx,
947 )
948 })
949 .await;
950 let snapshot_2 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
951 assert_eq!(
952 anchor_ranges_2
953 .iter()
954 .map(|range| range.to_point(&snapshot_2))
955 .collect::<Vec<_>>(),
956 vec![
957 Point::new(16, 1)..Point::new(17, 1),
958 Point::new(22, 0)..Point::new(22, 2)
959 ]
960 );
961
962 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
963 assert_eq!(
964 snapshot.text(),
965 concat!(
966 "bbb\n", // buffer_1
967 "ccc\n", //
968 "ddd\n", // <-- excerpt 1
969 "eee\n", // <-- excerpt 1
970 "fff\n", //
971 "ggg\n", //
972 "hhh\n", // <-- excerpt 2
973 "iii\n", //
974 "jjj\n", //
975 //
976 "nnn\n", //
977 "ooo\n", //
978 "ppp\n", // <-- excerpt 3
979 "qqq\n", //
980 "rrr\n", //
981 //
982 "aaaa\n", // buffer 2
983 "bbbb\n", //
984 "cccc\n", // <-- excerpt 4
985 "dddd\n", // <-- excerpt 4
986 "eeee\n", //
987 "ffff\n", //
988 //
989 "iiii\n", //
990 "jjjj\n", //
991 "kkkk\n", // <-- excerpt 5
992 "llll\n", //
993 "mmmm", //
994 )
995 );
996}
997
998#[gpui::test]
999fn test_empty_multibuffer(cx: &mut App) {
1000 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1001
1002 let snapshot = multibuffer.read(cx).snapshot(cx);
1003 assert_eq!(snapshot.text(), "");
1004 assert_eq!(
1005 snapshot
1006 .row_infos(MultiBufferRow(0))
1007 .map(|info| info.buffer_row)
1008 .collect::<Vec<_>>(),
1009 &[Some(0)]
1010 );
1011 assert!(
1012 snapshot
1013 .row_infos(MultiBufferRow(1))
1014 .map(|info| info.buffer_row)
1015 .collect::<Vec<_>>()
1016 .is_empty(),
1017 );
1018}
1019
1020#[gpui::test]
1021async fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
1022 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1023 let buffer = cx.new(|cx| Buffer::local("", cx));
1024 let base_text = "a\nb\nc";
1025
1026 let diff = cx
1027 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
1028 multibuffer.update(cx, |multibuffer, cx| {
1029 multibuffer.set_excerpt_ranges_for_path(
1030 PathKey::sorted(0),
1031 buffer.clone(),
1032 &buffer.read(cx).snapshot(),
1033 vec![ExcerptRange::new(Point::zero()..Point::zero())],
1034 cx,
1035 );
1036 multibuffer.set_all_diff_hunks_expanded(cx);
1037 multibuffer.add_diff(diff.clone(), cx);
1038 });
1039 cx.run_until_parked();
1040
1041 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
1042 assert_eq!(snapshot.text(), "a\nb\nc\n");
1043
1044 let hunk = snapshot
1045 .diff_hunks_in_range(Point::new(1, 1)..Point::new(1, 1))
1046 .next()
1047 .unwrap();
1048
1049 assert_eq!(hunk.diff_base_byte_range.start, BufferOffset(0));
1050
1051 let buf2 = cx.new(|cx| Buffer::local("X", cx));
1052 multibuffer.update(cx, |multibuffer, cx| {
1053 multibuffer.set_excerpts_for_path(
1054 PathKey::sorted(1),
1055 buf2,
1056 [Point::new(0, 0)..Point::new(0, 1)],
1057 0,
1058 cx,
1059 );
1060 });
1061
1062 buffer.update(cx, |buffer, cx| {
1063 buffer.edit([(0..0, "a\nb\nc")], None, cx);
1064 diff.update(cx, |diff, cx| {
1065 diff.recalculate_diff_sync(&buffer.text_snapshot(), cx);
1066 });
1067 assert_eq!(buffer.text(), "a\nb\nc")
1068 });
1069 cx.run_until_parked();
1070
1071 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
1072 assert_eq!(snapshot.text(), "a\nb\nc\nX");
1073
1074 buffer.update(cx, |buffer, cx| {
1075 buffer.undo(cx);
1076 diff.update(cx, |diff, cx| {
1077 diff.recalculate_diff_sync(&buffer.text_snapshot(), cx);
1078 });
1079 assert_eq!(buffer.text(), "")
1080 });
1081 cx.run_until_parked();
1082
1083 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
1084 assert_eq!(snapshot.text(), "a\nb\nc\n\nX");
1085}
1086
1087#[gpui::test]
1088fn test_singleton_multibuffer_anchors(cx: &mut App) {
1089 let buffer = cx.new(|cx| Buffer::local("abcd", cx));
1090 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1091 let old_snapshot = multibuffer.read(cx).snapshot(cx);
1092 buffer.update(cx, |buffer, cx| {
1093 buffer.edit([(0..0, "X")], None, cx);
1094 buffer.edit([(5..5, "Y")], None, cx);
1095 });
1096 let new_snapshot = multibuffer.read(cx).snapshot(cx);
1097
1098 assert_eq!(old_snapshot.text(), "abcd");
1099 assert_eq!(new_snapshot.text(), "XabcdY");
1100
1101 assert_eq!(
1102 old_snapshot
1103 .anchor_before(MultiBufferOffset(0))
1104 .to_offset(&new_snapshot),
1105 MultiBufferOffset(0)
1106 );
1107 assert_eq!(
1108 old_snapshot
1109 .anchor_after(MultiBufferOffset(0))
1110 .to_offset(&new_snapshot),
1111 MultiBufferOffset(1)
1112 );
1113 assert_eq!(
1114 old_snapshot
1115 .anchor_before(MultiBufferOffset(4))
1116 .to_offset(&new_snapshot),
1117 MultiBufferOffset(5)
1118 );
1119 assert_eq!(
1120 old_snapshot
1121 .anchor_after(MultiBufferOffset(4))
1122 .to_offset(&new_snapshot),
1123 MultiBufferOffset(6)
1124 );
1125}
1126
1127#[gpui::test]
1128fn test_multibuffer_anchors(cx: &mut App) {
1129 let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx));
1130 let buffer_2 = cx.new(|cx| Buffer::local("efghi", cx));
1131 let multibuffer = cx.new(|cx| {
1132 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1133 multibuffer.set_excerpts_for_path(
1134 PathKey::sorted(0),
1135 buffer_1.clone(),
1136 [Point::new(0, 0)..Point::new(0, 4)],
1137 0,
1138 cx,
1139 );
1140 multibuffer.set_excerpts_for_path(
1141 PathKey::sorted(1),
1142 buffer_2.clone(),
1143 [Point::new(0, 0)..Point::new(0, 5)],
1144 0,
1145 cx,
1146 );
1147 multibuffer
1148 });
1149 let old_snapshot = multibuffer.read(cx).snapshot(cx);
1150
1151 assert_eq!(
1152 old_snapshot
1153 .anchor_before(MultiBufferOffset(0))
1154 .to_offset(&old_snapshot),
1155 MultiBufferOffset(0)
1156 );
1157 assert_eq!(
1158 old_snapshot
1159 .anchor_after(MultiBufferOffset(0))
1160 .to_offset(&old_snapshot),
1161 MultiBufferOffset(0)
1162 );
1163 assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0));
1164 assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0));
1165 assert_eq!(
1166 Anchor::max().to_offset(&old_snapshot),
1167 MultiBufferOffset(10)
1168 );
1169 assert_eq!(
1170 Anchor::max().to_offset(&old_snapshot),
1171 MultiBufferOffset(10)
1172 );
1173
1174 buffer_1.update(cx, |buffer, cx| {
1175 buffer.edit([(0..0, "W")], None, cx);
1176 buffer.edit([(5..5, "X")], None, cx);
1177 });
1178 buffer_2.update(cx, |buffer, cx| {
1179 buffer.edit([(0..0, "Y")], None, cx);
1180 buffer.edit([(6..6, "Z")], None, cx);
1181 });
1182 let new_snapshot = multibuffer.read(cx).snapshot(cx);
1183
1184 assert_eq!(old_snapshot.text(), "abcd\nefghi");
1185 assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ");
1186
1187 assert_eq!(
1188 old_snapshot
1189 .anchor_before(MultiBufferOffset(0))
1190 .to_offset(&new_snapshot),
1191 MultiBufferOffset(0)
1192 );
1193 assert_eq!(
1194 old_snapshot
1195 .anchor_after(MultiBufferOffset(0))
1196 .to_offset(&new_snapshot),
1197 MultiBufferOffset(1)
1198 );
1199 assert_eq!(
1200 old_snapshot
1201 .anchor_before(MultiBufferOffset(1))
1202 .to_offset(&new_snapshot),
1203 MultiBufferOffset(2)
1204 );
1205 assert_eq!(
1206 old_snapshot
1207 .anchor_after(MultiBufferOffset(1))
1208 .to_offset(&new_snapshot),
1209 MultiBufferOffset(2)
1210 );
1211 assert_eq!(
1212 old_snapshot
1213 .anchor_before(MultiBufferOffset(2))
1214 .to_offset(&new_snapshot),
1215 MultiBufferOffset(3)
1216 );
1217 assert_eq!(
1218 old_snapshot
1219 .anchor_after(MultiBufferOffset(2))
1220 .to_offset(&new_snapshot),
1221 MultiBufferOffset(3)
1222 );
1223 assert_eq!(
1224 old_snapshot
1225 .anchor_before(MultiBufferOffset(5))
1226 .to_offset(&new_snapshot),
1227 MultiBufferOffset(7)
1228 );
1229 assert_eq!(
1230 old_snapshot
1231 .anchor_after(MultiBufferOffset(5))
1232 .to_offset(&new_snapshot),
1233 MultiBufferOffset(8)
1234 );
1235 assert_eq!(
1236 old_snapshot
1237 .anchor_before(MultiBufferOffset(10))
1238 .to_offset(&new_snapshot),
1239 MultiBufferOffset(13)
1240 );
1241 assert_eq!(
1242 old_snapshot
1243 .anchor_after(MultiBufferOffset(10))
1244 .to_offset(&new_snapshot),
1245 MultiBufferOffset(14)
1246 );
1247}
1248
1249#[gpui::test]
1250fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) {
1251 let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx));
1252 let buffer_2 = cx.new(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx));
1253 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1254
1255 // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
1256 // Add an excerpt from buffer 1 that spans this new insertion.
1257 buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx));
1258 let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| {
1259 let buffer_1_snapshot = buffer_1.read(cx).snapshot();
1260 multibuffer.set_excerpt_ranges_for_path(
1261 PathKey::sorted(0),
1262 buffer_1,
1263 &buffer_1_snapshot,
1264 vec![ExcerptRange::new((0..7).to_point(&buffer_1_snapshot))],
1265 cx,
1266 );
1267 multibuffer.excerpt_ids().into_iter().next().unwrap()
1268 });
1269
1270 let snapshot_1 = multibuffer.read(cx).snapshot(cx);
1271 assert_eq!(snapshot_1.text(), "abcd123");
1272
1273 // Replace the buffer 1 excerpt with new excerpts from buffer 2.
1274 let (excerpt_id_2, _excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
1275 multibuffer.remove_excerpts_for_path(PathKey::sorted(0), cx);
1276 let snapshot_2 = buffer_2.read(cx).snapshot();
1277 multibuffer.set_excerpt_ranges_for_path(
1278 PathKey::sorted(1),
1279 buffer_2.clone(),
1280 &buffer_2.read(cx).snapshot(),
1281 vec![
1282 ExcerptRange::new((0..4).to_point(&snapshot_2)),
1283 ExcerptRange::new((6..10).to_point(&snapshot_2)),
1284 ExcerptRange::new((12..16).to_point(&snapshot_2)),
1285 ],
1286 cx,
1287 );
1288 let mut ids = multibuffer
1289 .excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)
1290 .into_iter()
1291 .map(|(id, _, _)| id);
1292 (ids.next().unwrap(), ids.next().unwrap())
1293 });
1294 let snapshot_2 = multibuffer.read(cx).snapshot(cx);
1295 assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP");
1296
1297 // The old excerpt id doesn't get reused.
1298 assert_ne!(excerpt_id_2, excerpt_id_1);
1299
1300 // Resolve some anchors from the previous snapshot in the new snapshot.
1301 // The current excerpts are from a different buffer, so we don't attempt to
1302 // resolve the old text anchor in the new buffer.
1303 assert_eq!(
1304 snapshot_2.summary_for_anchor::<MultiBufferOffset>(
1305 &snapshot_1.anchor_before(MultiBufferOffset(2))
1306 ),
1307 MultiBufferOffset(0)
1308 );
1309 assert_eq!(
1310 snapshot_2.summaries_for_anchors::<MultiBufferOffset, _>(&[
1311 snapshot_1.anchor_before(MultiBufferOffset(2)),
1312 snapshot_1.anchor_after(MultiBufferOffset(3))
1313 ]),
1314 vec![MultiBufferOffset(0), MultiBufferOffset(0)]
1315 );
1316
1317 // Refresh anchors from the old snapshot. The return value indicates that both
1318 // anchors lost their original excerpt.
1319 let refresh = snapshot_2.refresh_anchors(&[
1320 snapshot_1.anchor_before(MultiBufferOffset(2)),
1321 snapshot_1.anchor_after(MultiBufferOffset(3)),
1322 ]);
1323 assert_eq!(
1324 refresh,
1325 &[
1326 (0, snapshot_2.anchor_before(MultiBufferOffset(0)), false),
1327 (1, snapshot_2.anchor_after(MultiBufferOffset(0)), false),
1328 ]
1329 );
1330
1331 // Replace the middle excerpt with a smaller excerpt in buffer 2,
1332 // that intersects the old excerpt.
1333 multibuffer.update(cx, |multibuffer, cx| {
1334 let snapshot_2 = buffer_2.read(cx).snapshot();
1335 multibuffer.set_excerpt_ranges_for_path(
1336 PathKey::sorted(1),
1337 buffer_2.clone(),
1338 &buffer_2.read(cx).snapshot(),
1339 vec![
1340 ExcerptRange::new((0..4).to_point(&snapshot_2)),
1341 ExcerptRange::new((12..16).to_point(&snapshot_2)),
1342 ],
1343 cx,
1344 );
1345 multibuffer.set_excerpt_ranges_for_path(
1346 PathKey::sorted(1),
1347 buffer_2.clone(),
1348 &buffer_2.read(cx).snapshot(),
1349 vec![
1350 ExcerptRange::new((0..4).to_point(&snapshot_2)),
1351 ExcerptRange::new((5..8).to_point(&snapshot_2)),
1352 ExcerptRange::new((12..16).to_point(&snapshot_2)),
1353 ],
1354 cx,
1355 );
1356 });
1357
1358 let snapshot_3 = multibuffer.read(cx).snapshot(cx);
1359 assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP");
1360
1361 // Resolve some anchors from the previous snapshot in the new snapshot.
1362 // The third anchor can't be resolved, since its excerpt has been removed,
1363 // so it resolves to the same position as its predecessor.
1364 let anchors = [
1365 snapshot_2.anchor_before(MultiBufferOffset(0)),
1366 snapshot_2.anchor_after(MultiBufferOffset(2)),
1367 snapshot_2.anchor_after(MultiBufferOffset(6)),
1368 snapshot_2.anchor_after(MultiBufferOffset(14)),
1369 ];
1370 assert_eq!(
1371 snapshot_3.summaries_for_anchors::<MultiBufferOffset, _>(&anchors),
1372 &[
1373 MultiBufferOffset(0),
1374 MultiBufferOffset(2),
1375 MultiBufferOffset(9),
1376 MultiBufferOffset(13)
1377 ]
1378 );
1379
1380 let new_anchors = snapshot_3.refresh_anchors(&anchors);
1381 assert_eq!(
1382 new_anchors.iter().map(|a| (a.0, a.2)).collect::<Vec<_>>(),
1383 &[(0, true), (1, true), (2, true), (3, true)]
1384 );
1385 assert_eq!(
1386 snapshot_3.summaries_for_anchors::<MultiBufferOffset, _>(new_anchors.iter().map(|a| &a.1)),
1387 &[
1388 MultiBufferOffset(0),
1389 MultiBufferOffset(2),
1390 MultiBufferOffset(7),
1391 MultiBufferOffset(13)
1392 ]
1393 );
1394}
1395
1396#[gpui::test]
1397async fn test_basic_diff_hunks(cx: &mut TestAppContext) {
1398 let text = indoc!(
1399 "
1400 ZERO
1401 one
1402 TWO
1403 three
1404 six
1405 "
1406 );
1407 let base_text = indoc!(
1408 "
1409 one
1410 two
1411 three
1412 four
1413 five
1414 six
1415 "
1416 );
1417
1418 let buffer = cx.new(|cx| Buffer::local(text, cx));
1419 let diff = cx
1420 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
1421 cx.run_until_parked();
1422
1423 let multibuffer = cx.new(|cx| {
1424 let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
1425 multibuffer.add_diff(diff.clone(), cx);
1426 multibuffer
1427 });
1428
1429 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
1430 (multibuffer.snapshot(cx), multibuffer.subscribe())
1431 });
1432 assert_eq!(
1433 snapshot.text(),
1434 indoc!(
1435 "
1436 ZERO
1437 one
1438 TWO
1439 three
1440 six
1441 "
1442 ),
1443 );
1444
1445 multibuffer.update(cx, |multibuffer, cx| {
1446 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1447 });
1448
1449 assert_new_snapshot(
1450 &multibuffer,
1451 &mut snapshot,
1452 &mut subscription,
1453 cx,
1454 indoc!(
1455 "
1456 + ZERO
1457 one
1458 - two
1459 + TWO
1460 three
1461 - four
1462 - five
1463 six
1464 "
1465 ),
1466 );
1467
1468 assert_eq!(
1469 snapshot
1470 .row_infos(MultiBufferRow(0))
1471 .map(|info| (info.buffer_row, info.diff_status))
1472 .collect::<Vec<_>>(),
1473 vec![
1474 (Some(0), Some(DiffHunkStatus::added_none())),
1475 (Some(1), None),
1476 (Some(1), Some(DiffHunkStatus::deleted_none())),
1477 (Some(2), Some(DiffHunkStatus::added_none())),
1478 (Some(3), None),
1479 (Some(3), Some(DiffHunkStatus::deleted_none())),
1480 (Some(4), Some(DiffHunkStatus::deleted_none())),
1481 (Some(4), None),
1482 (Some(5), None)
1483 ]
1484 );
1485
1486 assert_chunks_in_ranges(&snapshot);
1487 assert_consistent_line_numbers(&snapshot);
1488 assert_position_translation(&snapshot);
1489 assert_line_indents(&snapshot);
1490
1491 multibuffer.update(cx, |multibuffer, cx| {
1492 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx)
1493 });
1494 assert_new_snapshot(
1495 &multibuffer,
1496 &mut snapshot,
1497 &mut subscription,
1498 cx,
1499 indoc!(
1500 "
1501 ZERO
1502 one
1503 TWO
1504 three
1505 six
1506 "
1507 ),
1508 );
1509
1510 assert_chunks_in_ranges(&snapshot);
1511 assert_consistent_line_numbers(&snapshot);
1512 assert_position_translation(&snapshot);
1513 assert_line_indents(&snapshot);
1514
1515 // Expand the first diff hunk
1516 multibuffer.update(cx, |multibuffer, cx| {
1517 let position = multibuffer.read(cx).anchor_before(Point::new(2, 2));
1518 multibuffer.expand_diff_hunks(vec![position..position], cx)
1519 });
1520 assert_new_snapshot(
1521 &multibuffer,
1522 &mut snapshot,
1523 &mut subscription,
1524 cx,
1525 indoc!(
1526 "
1527 ZERO
1528 one
1529 - two
1530 + TWO
1531 three
1532 six
1533 "
1534 ),
1535 );
1536
1537 // Expand the second diff hunk
1538 multibuffer.update(cx, |multibuffer, cx| {
1539 let start = multibuffer.read(cx).anchor_before(Point::new(4, 0));
1540 let end = multibuffer.read(cx).anchor_before(Point::new(5, 0));
1541 multibuffer.expand_diff_hunks(vec![start..end], cx)
1542 });
1543 assert_new_snapshot(
1544 &multibuffer,
1545 &mut snapshot,
1546 &mut subscription,
1547 cx,
1548 indoc!(
1549 "
1550 ZERO
1551 one
1552 - two
1553 + TWO
1554 three
1555 - four
1556 - five
1557 six
1558 "
1559 ),
1560 );
1561
1562 assert_chunks_in_ranges(&snapshot);
1563 assert_consistent_line_numbers(&snapshot);
1564 assert_position_translation(&snapshot);
1565 assert_line_indents(&snapshot);
1566
1567 // Edit the buffer before the first hunk
1568 buffer.update(cx, |buffer, cx| {
1569 buffer.edit_via_marked_text(
1570 indoc!(
1571 "
1572 ZERO
1573 one« hundred
1574 thousand»
1575 TWO
1576 three
1577 six
1578 "
1579 ),
1580 None,
1581 cx,
1582 );
1583 });
1584 assert_new_snapshot(
1585 &multibuffer,
1586 &mut snapshot,
1587 &mut subscription,
1588 cx,
1589 indoc!(
1590 "
1591 ZERO
1592 one hundred
1593 thousand
1594 - two
1595 + TWO
1596 three
1597 - four
1598 - five
1599 six
1600 "
1601 ),
1602 );
1603
1604 assert_chunks_in_ranges(&snapshot);
1605 assert_consistent_line_numbers(&snapshot);
1606 assert_position_translation(&snapshot);
1607 assert_line_indents(&snapshot);
1608
1609 // Recalculate the diff, changing the first diff hunk.
1610 diff.update(cx, |diff, cx| {
1611 diff.recalculate_diff_sync(&buffer.read(cx).text_snapshot(), cx);
1612 });
1613 cx.run_until_parked();
1614 assert_new_snapshot(
1615 &multibuffer,
1616 &mut snapshot,
1617 &mut subscription,
1618 cx,
1619 indoc!(
1620 "
1621 ZERO
1622 one hundred
1623 thousand
1624 TWO
1625 three
1626 - four
1627 - five
1628 six
1629 "
1630 ),
1631 );
1632
1633 assert_eq!(
1634 snapshot
1635 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len())
1636 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
1637 .collect::<Vec<_>>(),
1638 &[0..4, 5..7]
1639 );
1640}
1641
1642#[gpui::test]
1643async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
1644 let text = indoc!(
1645 "
1646 one
1647 TWO
1648 THREE
1649 four
1650 FIVE
1651 six
1652 "
1653 );
1654 let base_text = indoc!(
1655 "
1656 one
1657 four
1658 five
1659 six
1660 "
1661 );
1662
1663 let buffer = cx.new(|cx| Buffer::local(text, cx));
1664 let diff = cx
1665 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
1666 cx.run_until_parked();
1667
1668 let multibuffer = cx.new(|cx| {
1669 let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
1670 multibuffer.add_diff(diff.clone(), cx);
1671 multibuffer
1672 });
1673
1674 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
1675 (multibuffer.snapshot(cx), multibuffer.subscribe())
1676 });
1677
1678 multibuffer.update(cx, |multibuffer, cx| {
1679 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1680 });
1681
1682 assert_new_snapshot(
1683 &multibuffer,
1684 &mut snapshot,
1685 &mut subscription,
1686 cx,
1687 indoc!(
1688 "
1689 one
1690 + TWO
1691 + THREE
1692 four
1693 - five
1694 + FIVE
1695 six
1696 "
1697 ),
1698 );
1699
1700 // Regression test: expanding diff hunks that are already expanded should not change anything.
1701 multibuffer.update(cx, |multibuffer, cx| {
1702 multibuffer.expand_diff_hunks(
1703 vec![
1704 snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)),
1705 ],
1706 cx,
1707 );
1708 });
1709
1710 assert_new_snapshot(
1711 &multibuffer,
1712 &mut snapshot,
1713 &mut subscription,
1714 cx,
1715 indoc!(
1716 "
1717 one
1718 + TWO
1719 + THREE
1720 four
1721 - five
1722 + FIVE
1723 six
1724 "
1725 ),
1726 );
1727
1728 // Now collapse all diff hunks
1729 multibuffer.update(cx, |multibuffer, cx| {
1730 multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
1731 });
1732
1733 assert_new_snapshot(
1734 &multibuffer,
1735 &mut snapshot,
1736 &mut subscription,
1737 cx,
1738 indoc!(
1739 "
1740 one
1741 TWO
1742 THREE
1743 four
1744 FIVE
1745 six
1746 "
1747 ),
1748 );
1749
1750 // Expand the hunks again, but this time provide two ranges that are both within the same hunk
1751 // Target the first hunk which is between "one" and "four"
1752 multibuffer.update(cx, |multibuffer, cx| {
1753 multibuffer.expand_diff_hunks(
1754 vec![
1755 snapshot.anchor_before(Point::new(4, 0))..snapshot.anchor_before(Point::new(4, 0)),
1756 snapshot.anchor_before(Point::new(4, 2))..snapshot.anchor_before(Point::new(4, 2)),
1757 ],
1758 cx,
1759 );
1760 });
1761 assert_new_snapshot(
1762 &multibuffer,
1763 &mut snapshot,
1764 &mut subscription,
1765 cx,
1766 indoc!(
1767 "
1768 one
1769 TWO
1770 THREE
1771 four
1772 - five
1773 + FIVE
1774 six
1775 "
1776 ),
1777 );
1778}
1779
1780#[gpui::test]
1781fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
1782 let buf1 = cx.new(|cx| {
1783 Buffer::local(
1784 indoc! {
1785 "zero
1786 one
1787 two
1788 two.five
1789 three
1790 four
1791 five
1792 six
1793 seven
1794 eight
1795 nine
1796 ten
1797 eleven
1798 ",
1799 },
1800 cx,
1801 )
1802 });
1803 let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
1804
1805 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1806 multibuffer.update(cx, |multibuffer, cx| {
1807 multibuffer.set_excerpts_for_path(
1808 path1.clone(),
1809 buf1.clone(),
1810 vec![
1811 Point::row_range(1..2),
1812 Point::row_range(6..7),
1813 Point::row_range(11..12),
1814 ],
1815 1,
1816 cx,
1817 );
1818 });
1819
1820 assert_excerpts_match(
1821 &multibuffer,
1822 cx,
1823 indoc! {
1824 "-----
1825 zero
1826 one
1827 two
1828 two.five
1829 -----
1830 four
1831 five
1832 six
1833 seven
1834 -----
1835 nine
1836 ten
1837 eleven
1838 "
1839 },
1840 );
1841
1842 buf1.update(cx, |buffer, cx| buffer.edit([(0..5, "")], None, cx));
1843
1844 multibuffer.update(cx, |multibuffer, cx| {
1845 multibuffer.set_excerpts_for_path(
1846 path1.clone(),
1847 buf1.clone(),
1848 vec![
1849 Point::row_range(0..3),
1850 Point::row_range(5..7),
1851 Point::row_range(10..11),
1852 ],
1853 1,
1854 cx,
1855 );
1856 });
1857
1858 assert_excerpts_match(
1859 &multibuffer,
1860 cx,
1861 indoc! {
1862 "-----
1863 one
1864 two
1865 two.five
1866 three
1867 four
1868 five
1869 six
1870 seven
1871 eight
1872 nine
1873 ten
1874 eleven
1875 "
1876 },
1877 );
1878}
1879
1880#[gpui::test]
1881fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
1882 let buf1 = cx.new(|cx| {
1883 Buffer::local(
1884 indoc! {
1885 "zero
1886 one
1887 two
1888 three
1889 four
1890 five
1891 six
1892 seven
1893 ",
1894 },
1895 cx,
1896 )
1897 });
1898 let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
1899 let buf2 = cx.new(|cx| {
1900 Buffer::local(
1901 indoc! {
1902 "000
1903 111
1904 222
1905 333
1906 444
1907 555
1908 666
1909 777
1910 888
1911 999
1912 "
1913 },
1914 cx,
1915 )
1916 });
1917 let path2 = PathKey::with_sort_prefix(1, rel_path("root").into_arc());
1918
1919 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
1920 multibuffer.update(cx, |multibuffer, cx| {
1921 multibuffer.set_excerpts_for_path(
1922 path1.clone(),
1923 buf1.clone(),
1924 vec![Point::row_range(0..1)],
1925 2,
1926 cx,
1927 );
1928 });
1929
1930 assert_excerpts_match(
1931 &multibuffer,
1932 cx,
1933 indoc! {
1934 "-----
1935 zero
1936 one
1937 two
1938 three
1939 "
1940 },
1941 );
1942
1943 multibuffer.update(cx, |multibuffer, cx| {
1944 multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
1945 });
1946
1947 assert_excerpts_match(&multibuffer, cx, "");
1948
1949 multibuffer.update(cx, |multibuffer, cx| {
1950 multibuffer.set_excerpts_for_path(
1951 path1.clone(),
1952 buf1.clone(),
1953 vec![Point::row_range(0..1), Point::row_range(7..8)],
1954 2,
1955 cx,
1956 );
1957 });
1958
1959 assert_excerpts_match(
1960 &multibuffer,
1961 cx,
1962 indoc! {"-----
1963 zero
1964 one
1965 two
1966 three
1967 -----
1968 five
1969 six
1970 seven
1971 "},
1972 );
1973
1974 multibuffer.update(cx, |multibuffer, cx| {
1975 multibuffer.set_excerpts_for_path(
1976 path1.clone(),
1977 buf1.clone(),
1978 vec![Point::row_range(0..1), Point::row_range(5..6)],
1979 2,
1980 cx,
1981 );
1982 });
1983
1984 assert_excerpts_match(
1985 &multibuffer,
1986 cx,
1987 indoc! {"-----
1988 zero
1989 one
1990 two
1991 three
1992 four
1993 five
1994 six
1995 seven
1996 "},
1997 );
1998
1999 multibuffer.update(cx, |multibuffer, cx| {
2000 multibuffer.set_excerpts_for_path(
2001 path2.clone(),
2002 buf2.clone(),
2003 vec![Point::row_range(2..3)],
2004 2,
2005 cx,
2006 );
2007 });
2008
2009 assert_excerpts_match(
2010 &multibuffer,
2011 cx,
2012 indoc! {"-----
2013 zero
2014 one
2015 two
2016 three
2017 four
2018 five
2019 six
2020 seven
2021 -----
2022 000
2023 111
2024 222
2025 333
2026 444
2027 555
2028 "},
2029 );
2030
2031 multibuffer.update(cx, |multibuffer, cx| {
2032 multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx);
2033 });
2034
2035 multibuffer.update(cx, |multibuffer, cx| {
2036 multibuffer.set_excerpts_for_path(
2037 path1.clone(),
2038 buf1.clone(),
2039 vec![Point::row_range(3..4)],
2040 2,
2041 cx,
2042 );
2043 });
2044
2045 assert_excerpts_match(
2046 &multibuffer,
2047 cx,
2048 indoc! {"-----
2049 one
2050 two
2051 three
2052 four
2053 five
2054 six
2055 -----
2056 000
2057 111
2058 222
2059 333
2060 444
2061 555
2062 "},
2063 );
2064
2065 multibuffer.update(cx, |multibuffer, cx| {
2066 multibuffer.set_excerpts_for_path(
2067 path1.clone(),
2068 buf1.clone(),
2069 vec![Point::row_range(3..4)],
2070 2,
2071 cx,
2072 );
2073 });
2074}
2075
2076#[gpui::test]
2077fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
2078 let buf1 = cx.new(|cx| {
2079 Buffer::local(
2080 indoc! {
2081 "zero
2082 one
2083 two
2084 three
2085 four
2086 five
2087 six
2088 seven
2089 ",
2090 },
2091 cx,
2092 )
2093 });
2094 let path: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
2095 let buf2 = cx.new(|cx| {
2096 Buffer::local(
2097 indoc! {
2098 "000
2099 111
2100 222
2101 333
2102 "
2103 },
2104 cx,
2105 )
2106 });
2107
2108 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2109 multibuffer.update(cx, |multibuffer, cx| {
2110 multibuffer.set_excerpts_for_path(
2111 path.clone(),
2112 buf1.clone(),
2113 vec![Point::row_range(1..1), Point::row_range(4..5)],
2114 1,
2115 cx,
2116 );
2117 });
2118
2119 assert_excerpts_match(
2120 &multibuffer,
2121 cx,
2122 indoc! {
2123 "-----
2124 zero
2125 one
2126 two
2127 three
2128 four
2129 five
2130 six
2131 "
2132 },
2133 );
2134
2135 multibuffer.update(cx, |multibuffer, cx| {
2136 multibuffer.set_excerpts_for_path(
2137 path.clone(),
2138 buf2.clone(),
2139 vec![Point::row_range(0..1)],
2140 2,
2141 cx,
2142 );
2143 });
2144
2145 assert_excerpts_match(
2146 &multibuffer,
2147 cx,
2148 indoc! {"-----
2149 000
2150 111
2151 222
2152 333
2153 "},
2154 );
2155}
2156
2157#[gpui::test]
2158async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
2159 let base_text_1 = indoc!(
2160 "
2161 one
2162 two
2163 three
2164 four
2165 five
2166 six
2167 "
2168 );
2169 let text_1 = indoc!(
2170 "
2171 ZERO
2172 one
2173 TWO
2174 three
2175 six
2176 "
2177 );
2178 let base_text_2 = indoc!(
2179 "
2180 seven
2181 eight
2182 nine
2183 ten
2184 eleven
2185 twelve
2186 "
2187 );
2188 let text_2 = indoc!(
2189 "
2190 eight
2191 nine
2192 eleven
2193 THIRTEEN
2194 FOURTEEN
2195 "
2196 );
2197
2198 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
2199 let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
2200 let diff_1 = cx.new(|cx| {
2201 BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
2202 });
2203 let diff_2 = cx.new(|cx| {
2204 BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx)
2205 });
2206 cx.run_until_parked();
2207
2208 let multibuffer = cx.new(|cx| {
2209 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2210 multibuffer.set_excerpts_for_path(
2211 PathKey::sorted(0),
2212 buffer_1.clone(),
2213 [Point::zero()..buffer_1.read(cx).max_point()],
2214 0,
2215 cx,
2216 );
2217 multibuffer.set_excerpts_for_path(
2218 PathKey::sorted(1),
2219 buffer_2.clone(),
2220 [Point::zero()..buffer_2.read(cx).max_point()],
2221 0,
2222 cx,
2223 );
2224 multibuffer.add_diff(diff_1.clone(), cx);
2225 multibuffer.add_diff(diff_2.clone(), cx);
2226 multibuffer
2227 });
2228
2229 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
2230 (multibuffer.snapshot(cx), multibuffer.subscribe())
2231 });
2232 assert_eq!(
2233 snapshot.text(),
2234 indoc!(
2235 "
2236 ZERO
2237 one
2238 TWO
2239 three
2240 six
2241
2242 eight
2243 nine
2244 eleven
2245 THIRTEEN
2246 FOURTEEN
2247 "
2248 ),
2249 );
2250
2251 multibuffer.update(cx, |multibuffer, cx| {
2252 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
2253 });
2254
2255 assert_new_snapshot(
2256 &multibuffer,
2257 &mut snapshot,
2258 &mut subscription,
2259 cx,
2260 indoc!(
2261 "
2262 + ZERO
2263 one
2264 - two
2265 + TWO
2266 three
2267 - four
2268 - five
2269 six
2270
2271 - seven
2272 eight
2273 nine
2274 - ten
2275 eleven
2276 - twelve
2277 + THIRTEEN
2278 + FOURTEEN
2279 "
2280 ),
2281 );
2282
2283 let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
2284 let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
2285 let base_id_1 = diff_1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
2286 let base_id_2 = diff_2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
2287
2288 let buffer_lines = (0..=snapshot.max_row().0)
2289 .map(|row| {
2290 let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?;
2291 Some((
2292 buffer.remote_id(),
2293 buffer.text_for_range(range).collect::<String>(),
2294 ))
2295 })
2296 .collect::<Vec<_>>();
2297 pretty_assertions::assert_eq!(
2298 buffer_lines,
2299 [
2300 Some((id_1, "ZERO".into())),
2301 Some((id_1, "one".into())),
2302 Some((base_id_1, "two".into())),
2303 Some((id_1, "TWO".into())),
2304 Some((id_1, " three".into())),
2305 Some((base_id_1, "four".into())),
2306 Some((base_id_1, "five".into())),
2307 Some((id_1, "six".into())),
2308 Some((id_1, "".into())),
2309 Some((base_id_2, "seven".into())),
2310 Some((id_2, " eight".into())),
2311 Some((id_2, "nine".into())),
2312 Some((base_id_2, "ten".into())),
2313 Some((id_2, "eleven".into())),
2314 Some((base_id_2, "twelve".into())),
2315 Some((id_2, "THIRTEEN".into())),
2316 Some((id_2, "FOURTEEN".into())),
2317 Some((id_2, "".into())),
2318 ]
2319 );
2320
2321 let buffer_ids_by_range = [
2322 (Point::new(0, 0)..Point::new(0, 0), &[id_1] as &[_]),
2323 (Point::new(0, 0)..Point::new(2, 0), &[id_1]),
2324 (Point::new(2, 0)..Point::new(2, 0), &[id_1]),
2325 (Point::new(3, 0)..Point::new(3, 0), &[id_1]),
2326 (Point::new(8, 0)..Point::new(9, 0), &[id_1]),
2327 (Point::new(8, 0)..Point::new(10, 0), &[id_1, id_2]),
2328 (Point::new(9, 0)..Point::new(9, 0), &[id_2]),
2329 ];
2330 for (range, buffer_ids) in buffer_ids_by_range {
2331 assert_eq!(
2332 snapshot
2333 .buffer_ids_for_range(range.clone())
2334 .collect::<Vec<_>>(),
2335 buffer_ids,
2336 "buffer_ids_for_range({range:?}"
2337 );
2338 }
2339
2340 assert_position_translation(&snapshot);
2341 assert_line_indents(&snapshot);
2342
2343 assert_eq!(
2344 snapshot
2345 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len())
2346 .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
2347 .collect::<Vec<_>>(),
2348 &[0..1, 2..4, 5..7, 9..10, 12..13, 14..17]
2349 );
2350
2351 buffer_2.update(cx, |buffer, cx| {
2352 buffer.edit_via_marked_text(
2353 indoc!(
2354 "
2355 eight
2356 «»eleven
2357 THIRTEEN
2358 FOURTEEN
2359 "
2360 ),
2361 None,
2362 cx,
2363 );
2364 });
2365
2366 assert_new_snapshot(
2367 &multibuffer,
2368 &mut snapshot,
2369 &mut subscription,
2370 cx,
2371 indoc!(
2372 "
2373 + ZERO
2374 one
2375 - two
2376 + TWO
2377 three
2378 - four
2379 - five
2380 six
2381
2382 - seven
2383 eight
2384 eleven
2385 - twelve
2386 + THIRTEEN
2387 + FOURTEEN
2388 "
2389 ),
2390 );
2391
2392 assert_line_indents(&snapshot);
2393}
2394
2395/// A naive implementation of a multi-buffer that does not maintain
2396/// any derived state, used for comparison in a randomized test.
2397#[derive(Default)]
2398struct ReferenceMultibuffer {
2399 excerpts: Vec<ReferenceExcerpt>,
2400 diffs: HashMap<BufferId, Entity<BufferDiff>>,
2401 inverted_diffs: HashMap<BufferId, (Entity<BufferDiff>, Entity<language::Buffer>)>,
2402}
2403
2404#[derive(Debug)]
2405struct ReferenceExcerpt {
2406 id: ExcerptId,
2407 buffer: Entity<Buffer>,
2408 range: Range<text::Anchor>,
2409 expanded_diff_hunks: Vec<text::Anchor>,
2410}
2411
2412#[derive(Debug)]
2413struct ReferenceRegion {
2414 buffer_id: Option<BufferId>,
2415 range: Range<usize>,
2416 buffer_range: Option<Range<Point>>,
2417 status: Option<DiffHunkStatus>,
2418 excerpt_id: Option<ExcerptId>,
2419}
2420
2421impl ReferenceMultibuffer {
2422 fn expand_excerpts(&mut self, excerpts: &HashSet<ExcerptId>, line_count: u32, cx: &App) {
2423 if line_count == 0 {
2424 return;
2425 }
2426
2427 for id in excerpts {
2428 let excerpt = self.excerpts.iter_mut().find(|e| e.id == *id).unwrap();
2429 let snapshot = excerpt.buffer.read(cx).snapshot();
2430 let mut point_range = excerpt.range.to_point(&snapshot);
2431 point_range.start = Point::new(point_range.start.row.saturating_sub(line_count), 0);
2432 point_range.end =
2433 snapshot.clip_point(Point::new(point_range.end.row + line_count, 0), Bias::Left);
2434 point_range.end.column = snapshot.line_len(point_range.end.row);
2435 excerpt.range =
2436 snapshot.anchor_before(point_range.start)..snapshot.anchor_after(point_range.end);
2437 }
2438 }
2439
2440 fn remove_excerpt(&mut self, id: ExcerptId, cx: &App) {
2441 let ix = self
2442 .excerpts
2443 .iter()
2444 .position(|excerpt| excerpt.id == id)
2445 .unwrap();
2446 let excerpt = self.excerpts.remove(ix);
2447 let buffer = excerpt.buffer.read(cx);
2448 let buffer_id = buffer.remote_id();
2449 log::info!(
2450 "Removing excerpt {}: {:?}",
2451 ix,
2452 buffer
2453 .text_for_range(excerpt.range.to_offset(buffer))
2454 .collect::<String>(),
2455 );
2456 if !self
2457 .excerpts
2458 .iter()
2459 .any(|excerpt| excerpt.buffer.read(cx).remote_id() == buffer_id)
2460 {
2461 self.diffs.remove(&buffer_id);
2462 self.inverted_diffs.remove(&buffer_id);
2463 }
2464 }
2465
2466 fn insert_excerpt_after(
2467 &mut self,
2468 prev_id: ExcerptId,
2469 new_excerpt_id: ExcerptId,
2470 (buffer_handle, anchor_range): (Entity<Buffer>, Range<text::Anchor>),
2471 ) {
2472 let excerpt_ix = if prev_id == ExcerptId::max() {
2473 self.excerpts.len()
2474 } else {
2475 self.excerpts
2476 .iter()
2477 .position(|excerpt| excerpt.id == prev_id)
2478 .unwrap()
2479 + 1
2480 };
2481 self.excerpts.insert(
2482 excerpt_ix,
2483 ReferenceExcerpt {
2484 id: new_excerpt_id,
2485 buffer: buffer_handle,
2486 range: anchor_range,
2487 expanded_diff_hunks: Vec::new(),
2488 },
2489 );
2490 }
2491
2492 fn expand_diff_hunks(&mut self, excerpt_id: ExcerptId, range: Range<text::Anchor>, cx: &App) {
2493 let excerpt = self
2494 .excerpts
2495 .iter_mut()
2496 .find(|e| e.id == excerpt_id)
2497 .unwrap();
2498 let buffer = excerpt.buffer.read(cx).snapshot();
2499 let buffer_id = buffer.remote_id();
2500
2501 // Skip inverted excerpts - hunks are always expanded
2502 if self.inverted_diffs.contains_key(&buffer_id) {
2503 return;
2504 }
2505
2506 let Some(diff) = self.diffs.get(&buffer_id) else {
2507 return;
2508 };
2509 let excerpt_range = excerpt.range.to_offset(&buffer);
2510 for hunk in diff
2511 .read(cx)
2512 .snapshot(cx)
2513 .hunks_intersecting_range(range, &buffer)
2514 {
2515 let hunk_range = hunk.buffer_range.to_offset(&buffer);
2516 if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end {
2517 continue;
2518 }
2519 if let Err(ix) = excerpt
2520 .expanded_diff_hunks
2521 .binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer))
2522 {
2523 log::info!(
2524 "expanding diff hunk {:?}. excerpt:{:?}, excerpt range:{:?}",
2525 hunk_range,
2526 excerpt_id,
2527 excerpt_range
2528 );
2529 excerpt
2530 .expanded_diff_hunks
2531 .insert(ix, hunk.buffer_range.start);
2532 } else {
2533 log::trace!("hunk {hunk_range:?} already expanded in excerpt {excerpt_id:?}");
2534 }
2535 }
2536 }
2537
2538 fn expected_content(&self, cx: &App) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
2539 let mut text = String::new();
2540 let mut regions = Vec::<ReferenceRegion>::new();
2541 let mut excerpt_boundary_rows = HashSet::default();
2542 for excerpt in &self.excerpts {
2543 excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32));
2544 let buffer = excerpt.buffer.read(cx);
2545 let buffer_id = buffer.remote_id();
2546 let buffer_range = excerpt.range.to_offset(buffer);
2547
2548 if let Some((diff, main_buffer)) = self.inverted_diffs.get(&buffer_id) {
2549 let diff_snapshot = diff.read(cx).snapshot(cx);
2550 let main_buffer_snapshot = main_buffer.read(cx).snapshot();
2551
2552 let mut offset = buffer_range.start;
2553 for hunk in diff_snapshot.hunks_intersecting_base_text_range(
2554 buffer_range.clone(),
2555 &main_buffer_snapshot.text,
2556 ) {
2557 let mut hunk_base_range = hunk.diff_base_byte_range.clone();
2558
2559 hunk_base_range.end = hunk_base_range.end.min(buffer_range.end);
2560 if hunk_base_range.start > buffer_range.end
2561 || hunk_base_range.start < buffer_range.start
2562 {
2563 continue;
2564 }
2565
2566 // Add the text before the hunk
2567 if hunk_base_range.start >= offset {
2568 let len = text.len();
2569 text.extend(buffer.text_for_range(offset..hunk_base_range.start));
2570 if text.len() > len {
2571 regions.push(ReferenceRegion {
2572 buffer_id: Some(buffer_id),
2573 range: len..text.len(),
2574 buffer_range: Some(
2575 (offset..hunk_base_range.start).to_point(&buffer),
2576 ),
2577 status: None,
2578 excerpt_id: Some(excerpt.id),
2579 });
2580 }
2581 }
2582
2583 // Add the "deleted" region (base text that's not in main)
2584 if !hunk_base_range.is_empty() {
2585 let len = text.len();
2586 text.extend(buffer.text_for_range(hunk_base_range.clone()));
2587 regions.push(ReferenceRegion {
2588 buffer_id: Some(buffer_id),
2589 range: len..text.len(),
2590 buffer_range: Some(hunk_base_range.to_point(&buffer)),
2591 status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
2592 excerpt_id: Some(excerpt.id),
2593 });
2594 }
2595
2596 offset = hunk_base_range.end;
2597 }
2598
2599 // Add remaining buffer text
2600 let len = text.len();
2601 text.extend(buffer.text_for_range(offset..buffer_range.end));
2602 text.push('\n');
2603 regions.push(ReferenceRegion {
2604 buffer_id: Some(buffer_id),
2605 range: len..text.len(),
2606 buffer_range: Some((offset..buffer_range.end).to_point(&buffer)),
2607 status: None,
2608 excerpt_id: Some(excerpt.id),
2609 });
2610 } else {
2611 let diff = self.diffs.get(&buffer_id).unwrap().read(cx).snapshot(cx);
2612 let base_buffer = diff.base_text();
2613
2614 let mut offset = buffer_range.start;
2615 let hunks = diff
2616 .hunks_intersecting_range(excerpt.range.clone(), buffer)
2617 .peekable();
2618
2619 for hunk in hunks {
2620 // Ignore hunks that are outside the excerpt range.
2621 let mut hunk_range = hunk.buffer_range.to_offset(buffer);
2622
2623 hunk_range.end = hunk_range.end.min(buffer_range.end);
2624 if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start
2625 {
2626 log::trace!("skipping hunk outside excerpt range");
2627 continue;
2628 }
2629
2630 if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| {
2631 expanded_anchor.to_offset(buffer).max(buffer_range.start)
2632 == hunk_range.start.max(buffer_range.start)
2633 }) {
2634 log::trace!("skipping a hunk that's not marked as expanded");
2635 continue;
2636 }
2637
2638 if !hunk.buffer_range.start.is_valid(buffer) {
2639 log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
2640 continue;
2641 }
2642
2643 if hunk_range.start >= offset {
2644 // Add the buffer text before the hunk
2645 let len = text.len();
2646 text.extend(buffer.text_for_range(offset..hunk_range.start));
2647 if text.len() > len {
2648 regions.push(ReferenceRegion {
2649 buffer_id: Some(buffer_id),
2650 range: len..text.len(),
2651 buffer_range: Some((offset..hunk_range.start).to_point(&buffer)),
2652 status: None,
2653 excerpt_id: Some(excerpt.id),
2654 });
2655 }
2656
2657 // Add the deleted text for the hunk.
2658 if !hunk.diff_base_byte_range.is_empty() {
2659 let mut base_text = base_buffer
2660 .text_for_range(hunk.diff_base_byte_range.clone())
2661 .collect::<String>();
2662 if !base_text.ends_with('\n') {
2663 base_text.push('\n');
2664 }
2665 let len = text.len();
2666 text.push_str(&base_text);
2667 regions.push(ReferenceRegion {
2668 buffer_id: Some(base_buffer.remote_id()),
2669 range: len..text.len(),
2670 buffer_range: Some(
2671 hunk.diff_base_byte_range.to_point(&base_buffer),
2672 ),
2673 status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
2674 excerpt_id: Some(excerpt.id),
2675 });
2676 }
2677
2678 offset = hunk_range.start;
2679 }
2680
2681 // Add the inserted text for the hunk.
2682 if hunk_range.end > offset {
2683 let len = text.len();
2684 text.extend(buffer.text_for_range(offset..hunk_range.end));
2685 let range = len..text.len();
2686 let region = ReferenceRegion {
2687 buffer_id: Some(buffer_id),
2688 range,
2689 buffer_range: Some((offset..hunk_range.end).to_point(&buffer)),
2690 status: Some(DiffHunkStatus::added(hunk.secondary_status)),
2691 excerpt_id: Some(excerpt.id),
2692 };
2693 offset = hunk_range.end;
2694 regions.push(region);
2695 }
2696 }
2697
2698 // Add the buffer text for the rest of the excerpt.
2699 let len = text.len();
2700 text.extend(buffer.text_for_range(offset..buffer_range.end));
2701 text.push('\n');
2702 regions.push(ReferenceRegion {
2703 buffer_id: Some(buffer_id),
2704 range: len..text.len(),
2705 buffer_range: Some((offset..buffer_range.end).to_point(&buffer)),
2706 status: None,
2707 excerpt_id: Some(excerpt.id),
2708 });
2709 }
2710 }
2711
2712 // Remove final trailing newline.
2713 if self.excerpts.is_empty() {
2714 regions.push(ReferenceRegion {
2715 buffer_id: None,
2716 range: 0..1,
2717 buffer_range: Some(Point::new(0, 0)..Point::new(0, 1)),
2718 status: None,
2719 excerpt_id: None,
2720 });
2721 } else {
2722 text.pop();
2723 }
2724
2725 // Retrieve the row info using the region that contains
2726 // the start of each multi-buffer line.
2727 let mut ix = 0;
2728 let row_infos = text
2729 .split('\n')
2730 .map(|line| {
2731 let row_info = regions
2732 .iter()
2733 .position(|region| region.range.contains(&ix))
2734 .map_or(RowInfo::default(), |region_ix| {
2735 let region = ®ions[region_ix];
2736 let buffer_row = region.buffer_range.as_ref().map(|buffer_range| {
2737 buffer_range.start.row
2738 + text[region.range.start..ix].matches('\n').count() as u32
2739 });
2740 let main_buffer = self
2741 .excerpts
2742 .iter()
2743 .find(|e| e.id == region.excerpt_id.unwrap())
2744 .map(|e| e.buffer.clone());
2745 let is_excerpt_start = region_ix == 0
2746 || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id
2747 || regions[region_ix - 1].range.is_empty();
2748 let mut is_excerpt_end = region_ix == regions.len() - 1
2749 || ®ions[region_ix + 1].excerpt_id != ®ion.excerpt_id;
2750 let is_start = !text[region.range.start..ix].contains('\n');
2751 let mut is_end = if region.range.end > text.len() {
2752 !text[ix..].contains('\n')
2753 } else {
2754 text[ix..region.range.end.min(text.len())]
2755 .matches('\n')
2756 .count()
2757 == 1
2758 };
2759 if region_ix < regions.len() - 1
2760 && !text[ix..].contains("\n")
2761 && (region.status == Some(DiffHunkStatus::added_none())
2762 || region.status.is_some_and(|s| s.is_deleted()))
2763 && regions[region_ix + 1].excerpt_id == region.excerpt_id
2764 && regions[region_ix + 1].range.start == text.len()
2765 {
2766 is_end = true;
2767 is_excerpt_end = true;
2768 }
2769 let multibuffer_row =
2770 MultiBufferRow(text[..ix].matches('\n').count() as u32);
2771 let mut expand_direction = None;
2772 if let Some(buffer) = &main_buffer {
2773 let buffer_row = buffer_row.unwrap();
2774 let needs_expand_up = is_excerpt_start && is_start && buffer_row > 0;
2775 let needs_expand_down = is_excerpt_end
2776 && is_end
2777 && buffer.read(cx).max_point().row > buffer_row;
2778 expand_direction = if needs_expand_up && needs_expand_down {
2779 Some(ExpandExcerptDirection::UpAndDown)
2780 } else if needs_expand_up {
2781 Some(ExpandExcerptDirection::Up)
2782 } else if needs_expand_down {
2783 Some(ExpandExcerptDirection::Down)
2784 } else {
2785 None
2786 };
2787 }
2788 RowInfo {
2789 buffer_id: region.buffer_id,
2790 diff_status: region.status,
2791 buffer_row,
2792 wrapped_buffer_row: None,
2793
2794 multibuffer_row: Some(multibuffer_row),
2795 expand_info: expand_direction.zip(region.excerpt_id).map(
2796 |(direction, excerpt_id)| ExpandInfo {
2797 direction,
2798 excerpt_id,
2799 },
2800 ),
2801 }
2802 });
2803 ix += line.len() + 1;
2804 row_info
2805 })
2806 .collect();
2807
2808 (text, row_infos, excerpt_boundary_rows)
2809 }
2810
2811 fn diffs_updated(&mut self, cx: &App) {
2812 for excerpt in &mut self.excerpts {
2813 let buffer = excerpt.buffer.read(cx).snapshot();
2814 let buffer_id = buffer.remote_id();
2815
2816 // Skip inverted diff excerpts - hunks are always expanded
2817 if self.inverted_diffs.contains_key(&buffer_id) {
2818 continue;
2819 }
2820
2821 let excerpt_range = excerpt.range.to_offset(&buffer);
2822 let Some(diff) = self.diffs.get(&buffer_id) else {
2823 continue;
2824 };
2825 let diff = diff.read(cx).snapshot(cx);
2826 let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable();
2827 excerpt.expanded_diff_hunks.retain(|hunk_anchor| {
2828 if !hunk_anchor.is_valid(&buffer) {
2829 return false;
2830 }
2831 while let Some(hunk) = hunks.peek() {
2832 match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) {
2833 cmp::Ordering::Less => {
2834 hunks.next();
2835 }
2836 cmp::Ordering::Equal => {
2837 let hunk_range = hunk.buffer_range.to_offset(&buffer);
2838 return hunk_range.end >= excerpt_range.start
2839 && hunk_range.start <= excerpt_range.end;
2840 }
2841 cmp::Ordering::Greater => break,
2842 }
2843 }
2844 false
2845 });
2846 }
2847 }
2848
2849 fn add_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut App) {
2850 let buffer_id = diff.read(cx).buffer_id;
2851 self.diffs.insert(buffer_id, diff);
2852 }
2853
2854 fn add_inverted_diff(
2855 &mut self,
2856 diff: Entity<BufferDiff>,
2857 main_buffer: Entity<language::Buffer>,
2858 cx: &App,
2859 ) {
2860 let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id();
2861 self.inverted_diffs
2862 .insert(base_text_buffer_id, (diff, main_buffer));
2863 }
2864}
2865
2866#[gpui::test(iterations = 100)]
2867async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
2868 let base_text = "a\n".repeat(100);
2869 let buf = cx.update(|cx| cx.new(|cx| Buffer::local(base_text, cx)));
2870 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2871
2872 let operations = env::var("OPERATIONS")
2873 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2874 .unwrap_or(10);
2875
2876 fn row_ranges(ranges: &Vec<Range<Point>>) -> Vec<Range<u32>> {
2877 ranges
2878 .iter()
2879 .map(|range| range.start.row..range.end.row)
2880 .collect()
2881 }
2882
2883 for _ in 0..operations {
2884 let snapshot = buf.update(cx, |buf, _| buf.snapshot());
2885 let num_ranges = rng.random_range(0..=10);
2886 let max_row = snapshot.max_point().row;
2887 let mut ranges = (0..num_ranges)
2888 .map(|_| {
2889 let start = rng.random_range(0..max_row);
2890 let end = rng.random_range(start + 1..max_row + 1);
2891 Point::row_range(start..end)
2892 })
2893 .collect::<Vec<_>>();
2894 ranges.sort_by_key(|range| range.start);
2895 log::info!("Setting ranges: {:?}", row_ranges(&ranges));
2896 let (created, _) = multibuffer.update(cx, |multibuffer, cx| {
2897 multibuffer.set_excerpts_for_path(
2898 PathKey::for_buffer(&buf, cx),
2899 buf.clone(),
2900 ranges.clone(),
2901 2,
2902 cx,
2903 )
2904 });
2905
2906 assert_eq!(created.len(), ranges.len());
2907
2908 let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
2909 let mut last_end = None;
2910 let mut seen_ranges = Vec::default();
2911
2912 for (_, buf, range) in snapshot.excerpts() {
2913 let start = range.context.start.to_point(buf);
2914 let end = range.context.end.to_point(buf);
2915 seen_ranges.push(start..end);
2916
2917 if let Some(last_end) = last_end.take() {
2918 assert!(
2919 start > last_end,
2920 "multibuffer has out-of-order ranges: {:?}; {:?} <= {:?}",
2921 row_ranges(&seen_ranges),
2922 start,
2923 last_end
2924 )
2925 }
2926
2927 ranges.retain(|range| range.start < start || range.end > end);
2928
2929 last_end = Some(end)
2930 }
2931
2932 assert!(
2933 ranges.is_empty(),
2934 "multibuffer {:?} did not include all ranges: {:?}",
2935 row_ranges(&seen_ranges),
2936 row_ranges(&ranges)
2937 );
2938 }
2939}
2940
2941#[gpui::test(iterations = 100)]
2942async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
2943 let operations = env::var("OPERATIONS")
2944 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2945 .unwrap_or(10);
2946 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2947 let mut buffers: Vec<Entity<Buffer>> = Vec::new();
2948 let mut base_texts: HashMap<BufferId, String> = HashMap::default();
2949 let mut reference = ReferenceMultibuffer::default();
2950 let mut anchors = Vec::new();
2951 let mut old_versions = Vec::new();
2952 let mut needs_diff_calculation = false;
2953 let mut inverted_diff_main_buffers: HashMap<BufferId, Entity<BufferDiff>> = HashMap::default();
2954 for _ in 0..operations {
2955 match rng.random_range(0..100) {
2956 0..=14 if !buffers.is_empty() => {
2957 let buffer = buffers.choose(&mut rng).unwrap();
2958 buffer.update(cx, |buf, cx| {
2959 let edit_count = rng.random_range(1..5);
2960 buf.randomly_edit(&mut rng, edit_count, cx);
2961 log::info!("buffer text:\n{}", buf.text());
2962 needs_diff_calculation = true;
2963 });
2964 cx.update(|cx| reference.diffs_updated(cx));
2965 }
2966 15..=19 if !reference.excerpts.is_empty() => {
2967 multibuffer.update(cx, |multibuffer, cx| {
2968 let ids = multibuffer.excerpt_ids();
2969 let mut excerpts = HashSet::default();
2970 for _ in 0..rng.random_range(0..ids.len()) {
2971 excerpts.extend(ids.choose(&mut rng).copied());
2972 }
2973
2974 let line_count = rng.random_range(0..5);
2975
2976 let excerpt_ixs = excerpts
2977 .iter()
2978 .map(|id| reference.excerpts.iter().position(|e| e.id == *id).unwrap())
2979 .collect::<Vec<_>>();
2980 log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
2981 multibuffer.expand_excerpts(
2982 excerpts.iter().cloned(),
2983 line_count,
2984 ExpandExcerptDirection::UpAndDown,
2985 cx,
2986 );
2987
2988 reference.expand_excerpts(&excerpts, line_count, cx);
2989 });
2990 }
2991 20..=29 if !reference.excerpts.is_empty() => {
2992 let mut ids_to_remove = vec![];
2993 for _ in 0..rng.random_range(1..=3) {
2994 let Some(excerpt) = reference.excerpts.choose(&mut rng) else {
2995 break;
2996 };
2997 let id = excerpt.id;
2998 cx.update(|cx| reference.remove_excerpt(id, cx));
2999 ids_to_remove.push(id);
3000 }
3001 let snapshot =
3002 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
3003 ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
3004 drop(snapshot);
3005 multibuffer.update(cx, |multibuffer, cx| {
3006 multibuffer.remove_excerpts(ids_to_remove, cx)
3007 });
3008 }
3009 30..=39 if !reference.excerpts.is_empty() => {
3010 let multibuffer =
3011 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
3012 let offset = multibuffer.clip_offset(
3013 MultiBufferOffset(rng.random_range(0..=multibuffer.len().0)),
3014 Bias::Left,
3015 );
3016 let bias = if rng.random() {
3017 Bias::Left
3018 } else {
3019 Bias::Right
3020 };
3021 log::info!("Creating anchor at {} with bias {:?}", offset.0, bias);
3022 anchors.push(multibuffer.anchor_at(offset, bias));
3023 anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
3024 }
3025 40..=44 if !anchors.is_empty() => {
3026 let multibuffer =
3027 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
3028 let prev_len = anchors.len();
3029 anchors = multibuffer
3030 .refresh_anchors(&anchors)
3031 .into_iter()
3032 .map(|a| a.1)
3033 .collect();
3034
3035 // Ensure the newly-refreshed anchors point to a valid excerpt and don't
3036 // overshoot its boundaries.
3037 assert_eq!(anchors.len(), prev_len);
3038 for anchor in &anchors {
3039 if anchor.excerpt_id == ExcerptId::min()
3040 || anchor.excerpt_id == ExcerptId::max()
3041 {
3042 continue;
3043 }
3044
3045 let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
3046 assert_eq!(excerpt.id, anchor.excerpt_id);
3047 assert!(excerpt.contains(anchor));
3048 }
3049 }
3050 45..=55 if !reference.excerpts.is_empty() => {
3051 multibuffer.update(cx, |multibuffer, cx| {
3052 let snapshot = multibuffer.snapshot(cx);
3053 let excerpt_ix = rng.random_range(0..reference.excerpts.len());
3054 let excerpt = &reference.excerpts[excerpt_ix];
3055
3056 // Skip inverted excerpts - hunks can't be collapsed
3057 let buffer_id = excerpt.buffer.read(cx).remote_id();
3058 if reference.inverted_diffs.contains_key(&buffer_id) {
3059 return;
3060 }
3061
3062 let start = excerpt.range.start;
3063 let end = excerpt.range.end;
3064 let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap()
3065 ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap();
3066
3067 log::info!(
3068 "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})",
3069 range.to_offset(&snapshot),
3070 excerpt.id,
3071 buffer_id,
3072 );
3073 reference.expand_diff_hunks(excerpt.id, start..end, cx);
3074 multibuffer.expand_diff_hunks(vec![range], cx);
3075 });
3076 }
3077 56..=85 if needs_diff_calculation => {
3078 multibuffer.update(cx, |multibuffer, cx| {
3079 for buffer in multibuffer.all_buffers() {
3080 let snapshot = buffer.read(cx).snapshot();
3081 let buffer_id = snapshot.remote_id();
3082
3083 if let Some(diff) = multibuffer.diff_for(buffer_id) {
3084 diff.update(cx, |diff, cx| {
3085 log::info!("recalculating diff for buffer {:?}", buffer_id,);
3086 diff.recalculate_diff_sync(&snapshot.text, cx);
3087 });
3088 }
3089
3090 if let Some(inverted_diff) = inverted_diff_main_buffers.get(&buffer_id) {
3091 inverted_diff.update(cx, |diff, cx| {
3092 log::info!(
3093 "recalculating inverted diff for main buffer {:?}",
3094 buffer_id,
3095 );
3096 diff.recalculate_diff_sync(&snapshot.text, cx);
3097 });
3098 }
3099 }
3100 reference.diffs_updated(cx);
3101 needs_diff_calculation = false;
3102 });
3103 }
3104 _ => {
3105 // Decide if we're creating a new buffer or reusing an existing one
3106 let create_new_buffer = buffers.is_empty() || rng.random_bool(0.4);
3107
3108 let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len());
3109 let prev_excerpt_id = reference
3110 .excerpts
3111 .get(prev_excerpt_ix)
3112 .map_or(ExcerptId::max(), |e| e.id);
3113 let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len());
3114
3115 let (excerpt_buffer, diff, inverted_main_buffer) = if create_new_buffer {
3116 let create_inverted = rng.random_bool(0.3);
3117
3118 if create_inverted {
3119 let mut main_buffer_text = util::RandomCharIter::new(&mut rng)
3120 .take(256)
3121 .collect::<String>();
3122 let main_buffer = cx.new(|cx| Buffer::local(main_buffer_text.clone(), cx));
3123 text::LineEnding::normalize(&mut main_buffer_text);
3124 let main_buffer_id =
3125 main_buffer.read_with(cx, |buffer, _| buffer.remote_id());
3126 base_texts.insert(main_buffer_id, main_buffer_text.clone());
3127 buffers.push(main_buffer.clone());
3128
3129 let diff = cx.new(|cx| {
3130 BufferDiff::new_with_base_text(
3131 &main_buffer_text,
3132 &main_buffer.read(cx).text_snapshot(),
3133 cx,
3134 )
3135 });
3136
3137 let base_text_buffer =
3138 diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
3139
3140 // Track for recalculation when main buffer is edited
3141 inverted_diff_main_buffers.insert(main_buffer_id, diff.clone());
3142
3143 (base_text_buffer, diff, Some(main_buffer))
3144 } else {
3145 let mut base_text = util::RandomCharIter::new(&mut rng)
3146 .take(256)
3147 .collect::<String>();
3148
3149 let buffer_handle = cx.new(|cx| Buffer::local(base_text.clone(), cx));
3150 text::LineEnding::normalize(&mut base_text);
3151 let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id());
3152 base_texts.insert(buffer_id, base_text.clone());
3153 buffers.push(buffer_handle.clone());
3154
3155 let diff = cx.new(|cx| {
3156 BufferDiff::new_with_base_text(
3157 &base_text,
3158 &buffer_handle.read(cx).text_snapshot(),
3159 cx,
3160 )
3161 });
3162
3163 (buffer_handle, diff, None)
3164 }
3165 } else {
3166 // Reuse an existing buffer
3167 let buffer_handle = buffers.choose(&mut rng).unwrap().clone();
3168 let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id());
3169
3170 if let Some(diff) = inverted_diff_main_buffers.get(&buffer_id) {
3171 let base_text_buffer =
3172 diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
3173 (base_text_buffer, diff.clone(), Some(buffer_handle))
3174 } else {
3175 // Get existing diff or create new one for regular buffer
3176 let diff = multibuffer
3177 .read_with(cx, |mb, _| mb.diff_for(buffer_id))
3178 .unwrap_or_else(|| {
3179 let base_text = base_texts.get(&buffer_id).unwrap();
3180 cx.new(|cx| {
3181 BufferDiff::new_with_base_text(
3182 base_text,
3183 &buffer_handle.read(cx).text_snapshot(),
3184 cx,
3185 )
3186 })
3187 });
3188 (buffer_handle, diff, None)
3189 }
3190 };
3191
3192 let (range, anchor_range) = excerpt_buffer.read_with(cx, |buffer, _| {
3193 let end_row = rng.random_range(0..=buffer.max_point().row);
3194 let start_row = rng.random_range(0..=end_row);
3195 let end_ix = buffer.point_to_offset(Point::new(end_row, 0));
3196 let start_ix = buffer.point_to_offset(Point::new(start_row, 0));
3197 let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
3198
3199 log::info!(
3200 "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
3201 excerpt_ix,
3202 reference.excerpts.len(),
3203 buffer.remote_id(),
3204 buffer.text(),
3205 start_ix..end_ix,
3206 &buffer.text()[start_ix..end_ix]
3207 );
3208
3209 (start_ix..end_ix, anchor_range)
3210 });
3211
3212 let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
3213 multibuffer
3214 .insert_excerpts_after(
3215 prev_excerpt_id,
3216 excerpt_buffer.clone(),
3217 [ExcerptRange::new(range.clone())],
3218 cx,
3219 )
3220 .pop()
3221 .unwrap()
3222 });
3223
3224 reference.insert_excerpt_after(
3225 prev_excerpt_id,
3226 excerpt_id,
3227 (excerpt_buffer.clone(), anchor_range),
3228 );
3229
3230 let excerpt_buffer_id =
3231 excerpt_buffer.read_with(cx, |buffer, _| buffer.remote_id());
3232 multibuffer.update(cx, |multibuffer, cx| {
3233 if multibuffer.diff_for(excerpt_buffer_id).is_none() {
3234 if let Some(main_buffer) = inverted_main_buffer {
3235 reference.add_inverted_diff(diff.clone(), main_buffer.clone(), cx);
3236 multibuffer.add_inverted_diff(diff, main_buffer, cx);
3237 } else {
3238 reference.add_diff(diff.clone(), cx);
3239 multibuffer.add_diff(diff, cx);
3240 }
3241 }
3242 });
3243 }
3244 }
3245
3246 if rng.random_bool(0.3) {
3247 multibuffer.update(cx, |multibuffer, cx| {
3248 old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
3249 })
3250 }
3251
3252 multibuffer.read_with(cx, |multibuffer, cx| {
3253 check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng);
3254 });
3255 }
3256 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
3257 for (old_snapshot, subscription) in old_versions {
3258 check_multibuffer_edits(&snapshot, &old_snapshot, subscription);
3259 }
3260}
3261
3262fn check_multibuffer(
3263 multibuffer: &MultiBuffer,
3264 reference: &ReferenceMultibuffer,
3265 anchors: &[Anchor],
3266 cx: &App,
3267 rng: &mut StdRng,
3268) {
3269 let snapshot = multibuffer.snapshot(cx);
3270 let actual_text = snapshot.text();
3271 let actual_boundary_rows = snapshot
3272 .excerpt_boundaries_in_range(MultiBufferOffset(0)..)
3273 .map(|b| b.row)
3274 .collect::<HashSet<_>>();
3275 let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
3276
3277 let (expected_text, expected_row_infos, expected_boundary_rows) =
3278 reference.expected_content(cx);
3279
3280 let has_diff = actual_row_infos
3281 .iter()
3282 .any(|info| info.diff_status.is_some())
3283 || expected_row_infos
3284 .iter()
3285 .any(|info| info.diff_status.is_some());
3286 let actual_diff = format_diff(
3287 &actual_text,
3288 &actual_row_infos,
3289 &actual_boundary_rows,
3290 Some(has_diff),
3291 );
3292 let expected_diff = format_diff(
3293 &expected_text,
3294 &expected_row_infos,
3295 &expected_boundary_rows,
3296 Some(has_diff),
3297 );
3298
3299 log::info!("Multibuffer content:\n{}", actual_diff);
3300
3301 assert_eq!(
3302 actual_row_infos.len(),
3303 actual_text.split('\n').count(),
3304 "line count: {}",
3305 actual_text.split('\n').count()
3306 );
3307 pretty_assertions::assert_eq!(actual_diff, expected_diff);
3308 pretty_assertions::assert_eq!(actual_text, expected_text);
3309 pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
3310
3311 for _ in 0..5 {
3312 let start_row = rng.random_range(0..=expected_row_infos.len());
3313 assert_eq!(
3314 snapshot
3315 .row_infos(MultiBufferRow(start_row as u32))
3316 .collect::<Vec<_>>(),
3317 &expected_row_infos[start_row..],
3318 "buffer_rows({})",
3319 start_row
3320 );
3321 }
3322
3323 assert_eq!(
3324 snapshot.widest_line_number(),
3325 expected_row_infos
3326 .into_iter()
3327 .filter_map(|info| {
3328 // For inverted diffs, deleted rows are visible and should be counted.
3329 // Only filter out deleted rows that are NOT from inverted diffs.
3330 let is_inverted_diff = info
3331 .buffer_id
3332 .is_some_and(|id| reference.inverted_diffs.contains_key(&id));
3333 if info.diff_status.is_some_and(|status| status.is_deleted()) && !is_inverted_diff {
3334 None
3335 } else {
3336 info.buffer_row
3337 }
3338 })
3339 .max()
3340 .unwrap()
3341 + 1
3342 );
3343 let reference_ranges = reference
3344 .excerpts
3345 .iter()
3346 .map(|excerpt| {
3347 (
3348 excerpt.id,
3349 excerpt.range.to_offset(&excerpt.buffer.read(cx).snapshot()),
3350 )
3351 })
3352 .collect::<HashMap<_, _>>();
3353 for i in 0..snapshot.len().0 {
3354 let excerpt = snapshot
3355 .excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i))
3356 .unwrap();
3357 assert_eq!(
3358 excerpt.buffer_range().start.0..excerpt.buffer_range().end.0,
3359 reference_ranges[&excerpt.id()]
3360 );
3361 }
3362
3363 assert_consistent_line_numbers(&snapshot);
3364 assert_position_translation(&snapshot);
3365
3366 for (row, line) in expected_text.split('\n').enumerate() {
3367 assert_eq!(
3368 snapshot.line_len(MultiBufferRow(row as u32)),
3369 line.len() as u32,
3370 "line_len({}).",
3371 row
3372 );
3373 }
3374
3375 let text_rope = Rope::from(expected_text.as_str());
3376 for _ in 0..10 {
3377 let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
3378 let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
3379
3380 let text_for_range = snapshot
3381 .text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix))
3382 .collect::<String>();
3383 assert_eq!(
3384 text_for_range,
3385 &expected_text[start_ix..end_ix],
3386 "incorrect text for range {:?}",
3387 start_ix..end_ix
3388 );
3389
3390 let expected_summary =
3391 MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix]));
3392 assert_eq!(
3393 snapshot.text_summary_for_range::<MBTextSummary, _>(
3394 MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)
3395 ),
3396 expected_summary,
3397 "incorrect summary for range {:?}",
3398 start_ix..end_ix
3399 );
3400 }
3401
3402 // Anchor resolution
3403 let summaries = snapshot.summaries_for_anchors::<MultiBufferOffset, _>(anchors);
3404 assert_eq!(anchors.len(), summaries.len());
3405 for (anchor, resolved_offset) in anchors.iter().zip(summaries) {
3406 assert!(resolved_offset <= snapshot.len());
3407 assert_eq!(
3408 snapshot.summary_for_anchor::<MultiBufferOffset>(anchor),
3409 resolved_offset,
3410 "anchor: {:?}",
3411 anchor
3412 );
3413 }
3414
3415 for _ in 0..10 {
3416 let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
3417 assert_eq!(
3418 snapshot
3419 .reversed_chars_at(MultiBufferOffset(end_ix))
3420 .collect::<String>(),
3421 expected_text[..end_ix].chars().rev().collect::<String>(),
3422 );
3423 }
3424
3425 for _ in 0..10 {
3426 let end_ix = rng.random_range(0..=text_rope.len());
3427 let end_ix = text_rope.floor_char_boundary(end_ix);
3428 let start_ix = rng.random_range(0..=end_ix);
3429 let start_ix = text_rope.floor_char_boundary(start_ix);
3430 assert_eq!(
3431 snapshot
3432 .bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix))
3433 .flatten()
3434 .copied()
3435 .collect::<Vec<_>>(),
3436 expected_text.as_bytes()[start_ix..end_ix].to_vec(),
3437 "bytes_in_range({:?})",
3438 start_ix..end_ix,
3439 );
3440 }
3441}
3442
3443fn check_multibuffer_edits(
3444 snapshot: &MultiBufferSnapshot,
3445 old_snapshot: &MultiBufferSnapshot,
3446 subscription: Subscription<MultiBufferOffset>,
3447) {
3448 let edits = subscription.consume().into_inner();
3449
3450 log::info!(
3451 "applying subscription edits to old text: {:?}: {:#?}",
3452 old_snapshot.text(),
3453 edits,
3454 );
3455
3456 let mut text = old_snapshot.text();
3457 for edit in edits {
3458 let new_text: String = snapshot
3459 .text_for_range(edit.new.start..edit.new.end)
3460 .collect();
3461 text.replace_range(
3462 (edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0)).clone(),
3463 &new_text,
3464 );
3465 pretty_assertions::assert_eq!(
3466 &text[0..edit.new.end.0],
3467 snapshot
3468 .text_for_range(MultiBufferOffset(0)..edit.new.end)
3469 .collect::<String>()
3470 );
3471 }
3472 pretty_assertions::assert_eq!(text, snapshot.text());
3473}
3474
3475#[gpui::test]
3476fn test_history(cx: &mut App) {
3477 let test_settings = SettingsStore::test(cx);
3478 cx.set_global(test_settings);
3479
3480 let group_interval: Duration = Duration::from_millis(1);
3481 let buffer_1 = cx.new(|cx| {
3482 let mut buf = Buffer::local("1234", cx);
3483 buf.set_group_interval(group_interval);
3484 buf
3485 });
3486 let buffer_2 = cx.new(|cx| {
3487 let mut buf = Buffer::local("5678", cx);
3488 buf.set_group_interval(group_interval);
3489 buf
3490 });
3491 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
3492 multibuffer.update(cx, |this, _| {
3493 this.set_group_interval(group_interval);
3494 });
3495 multibuffer.update(cx, |multibuffer, cx| {
3496 multibuffer.set_excerpts_for_path(
3497 PathKey::sorted(0),
3498 buffer_1.clone(),
3499 [Point::zero()..buffer_1.read(cx).max_point()],
3500 0,
3501 cx,
3502 );
3503 multibuffer.set_excerpts_for_path(
3504 PathKey::sorted(1),
3505 buffer_2.clone(),
3506 [Point::zero()..buffer_2.read(cx).max_point()],
3507 0,
3508 cx,
3509 );
3510 });
3511
3512 let mut now = Instant::now();
3513
3514 multibuffer.update(cx, |multibuffer, cx| {
3515 let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap();
3516 multibuffer.edit(
3517 [
3518 (Point::new(0, 0)..Point::new(0, 0), "A"),
3519 (Point::new(1, 0)..Point::new(1, 0), "A"),
3520 ],
3521 None,
3522 cx,
3523 );
3524 multibuffer.edit(
3525 [
3526 (Point::new(0, 1)..Point::new(0, 1), "B"),
3527 (Point::new(1, 1)..Point::new(1, 1), "B"),
3528 ],
3529 None,
3530 cx,
3531 );
3532 multibuffer.end_transaction_at(now, cx);
3533 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3534
3535 // Verify edited ranges for transaction 1
3536 assert_eq!(
3537 multibuffer.edited_ranges_for_transaction(transaction_1, cx),
3538 &[
3539 Point::new(0, 0)..Point::new(0, 2),
3540 Point::new(1, 0)..Point::new(1, 2)
3541 ]
3542 );
3543
3544 // Edit buffer 1 through the multibuffer
3545 now += 2 * group_interval;
3546 multibuffer.start_transaction_at(now, cx);
3547 multibuffer.edit(
3548 [(MultiBufferOffset(2)..MultiBufferOffset(2), "C")],
3549 None,
3550 cx,
3551 );
3552 multibuffer.end_transaction_at(now, cx);
3553 assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678");
3554
3555 // Edit buffer 1 independently
3556 buffer_1.update(cx, |buffer_1, cx| {
3557 buffer_1.start_transaction_at(now);
3558 buffer_1.edit([(3..3, "D")], None, cx);
3559 buffer_1.end_transaction_at(now, cx);
3560
3561 now += 2 * group_interval;
3562 buffer_1.start_transaction_at(now);
3563 buffer_1.edit([(4..4, "E")], None, cx);
3564 buffer_1.end_transaction_at(now, cx);
3565 });
3566 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
3567
3568 // An undo in the multibuffer undoes the multibuffer transaction
3569 // and also any individual buffer edits that have occurred since
3570 // that transaction.
3571 multibuffer.undo(cx);
3572 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3573
3574 multibuffer.undo(cx);
3575 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3576
3577 multibuffer.redo(cx);
3578 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3579
3580 multibuffer.redo(cx);
3581 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
3582
3583 // Undo buffer 2 independently.
3584 buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx));
3585 assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678");
3586
3587 // An undo in the multibuffer undoes the components of the
3588 // the last multibuffer transaction that are not already undone.
3589 multibuffer.undo(cx);
3590 assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678");
3591
3592 multibuffer.undo(cx);
3593 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3594
3595 multibuffer.redo(cx);
3596 assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
3597
3598 buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx));
3599 assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
3600
3601 // Redo stack gets cleared after an edit.
3602 now += 2 * group_interval;
3603 multibuffer.start_transaction_at(now, cx);
3604 multibuffer.edit(
3605 [(MultiBufferOffset(0)..MultiBufferOffset(0), "X")],
3606 None,
3607 cx,
3608 );
3609 multibuffer.end_transaction_at(now, cx);
3610 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3611 multibuffer.redo(cx);
3612 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3613 multibuffer.undo(cx);
3614 assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
3615 multibuffer.undo(cx);
3616 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3617
3618 // Transactions can be grouped manually.
3619 multibuffer.redo(cx);
3620 multibuffer.redo(cx);
3621 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3622 multibuffer.group_until_transaction(transaction_1, cx);
3623 multibuffer.undo(cx);
3624 assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
3625 multibuffer.redo(cx);
3626 assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
3627 });
3628}
3629
3630#[gpui::test]
3631async fn test_enclosing_indent(cx: &mut TestAppContext) {
3632 async fn enclosing_indent(
3633 text: &str,
3634 buffer_row: u32,
3635 cx: &mut TestAppContext,
3636 ) -> Option<(Range<u32>, LineIndent)> {
3637 let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
3638 let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx));
3639 let (range, indent) = snapshot
3640 .enclosing_indent(MultiBufferRow(buffer_row))
3641 .await?;
3642 Some((range.start.0..range.end.0, indent))
3643 }
3644
3645 assert_eq!(
3646 enclosing_indent(
3647 indoc!(
3648 "
3649 fn b() {
3650 if c {
3651 let d = 2;
3652 }
3653 }
3654 "
3655 ),
3656 1,
3657 cx,
3658 )
3659 .await,
3660 Some((
3661 1..2,
3662 LineIndent {
3663 tabs: 0,
3664 spaces: 4,
3665 line_blank: false,
3666 }
3667 ))
3668 );
3669
3670 assert_eq!(
3671 enclosing_indent(
3672 indoc!(
3673 "
3674 fn b() {
3675 if c {
3676 let d = 2;
3677 }
3678 }
3679 "
3680 ),
3681 2,
3682 cx,
3683 )
3684 .await,
3685 Some((
3686 1..2,
3687 LineIndent {
3688 tabs: 0,
3689 spaces: 4,
3690 line_blank: false,
3691 }
3692 ))
3693 );
3694
3695 assert_eq!(
3696 enclosing_indent(
3697 indoc!(
3698 "
3699 fn b() {
3700 if c {
3701 let d = 2;
3702
3703 let e = 5;
3704 }
3705 }
3706 "
3707 ),
3708 3,
3709 cx,
3710 )
3711 .await,
3712 Some((
3713 1..4,
3714 LineIndent {
3715 tabs: 0,
3716 spaces: 4,
3717 line_blank: false,
3718 }
3719 ))
3720 );
3721}
3722
3723#[gpui::test]
3724async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
3725 let base_text_1 = indoc!(
3726 "
3727 bar
3728 "
3729 );
3730 let text_1 = indoc!(
3731 "
3732 BAR
3733 "
3734 );
3735 let base_text_2 = indoc!(
3736 "
3737 foo
3738 "
3739 );
3740 let text_2 = indoc!(
3741 "
3742 FOO
3743 "
3744 );
3745
3746 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
3747 let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
3748 let diff_1 = cx.new(|cx| {
3749 BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
3750 });
3751 let diff_2 = cx.new(|cx| {
3752 BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx)
3753 });
3754 cx.run_until_parked();
3755
3756 let mut ids = vec![];
3757 let multibuffer = cx.new(|cx| {
3758 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
3759 multibuffer.set_all_diff_hunks_expanded(cx);
3760 multibuffer.set_excerpts_for_path(
3761 PathKey::sorted(0),
3762 buffer_1.clone(),
3763 [Point::zero()..buffer_1.read(cx).max_point()],
3764 0,
3765 cx,
3766 );
3767 multibuffer.set_excerpts_for_path(
3768 PathKey::sorted(1),
3769 buffer_2.clone(),
3770 [Point::zero()..buffer_2.read(cx).max_point()],
3771 0,
3772 cx,
3773 );
3774 multibuffer.add_diff(diff_1.clone(), cx);
3775 multibuffer.add_diff(diff_2.clone(), cx);
3776 ids = multibuffer.excerpt_ids();
3777 multibuffer
3778 });
3779
3780 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
3781 (multibuffer.snapshot(cx), multibuffer.subscribe())
3782 });
3783
3784 assert_new_snapshot(
3785 &multibuffer,
3786 &mut snapshot,
3787 &mut subscription,
3788 cx,
3789 indoc!(
3790 "
3791 - bar
3792 + BAR
3793
3794 - foo
3795 + FOO
3796 "
3797 ),
3798 );
3799
3800 let anchor_1 = Anchor::in_buffer(ids[0], text::Anchor::MIN);
3801 let point_1 = snapshot.summaries_for_anchors::<Point, _>([&anchor_1])[0];
3802 assert_eq!(point_1, Point::new(0, 0));
3803
3804 let anchor_2 = Anchor::in_buffer(ids[1], text::Anchor::MIN);
3805 let point_2 = snapshot.summaries_for_anchors::<Point, _>([&anchor_2])[0];
3806 assert_eq!(point_2, Point::new(3, 0));
3807}
3808
3809#[gpui::test]
3810async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
3811 let base_text_1 = "one\ntwo".to_owned();
3812 let text_1 = "one\n".to_owned();
3813
3814 let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
3815 let diff_1 = cx.new(|cx| {
3816 BufferDiff::new_with_base_text(&base_text_1, &buffer_1.read(cx).text_snapshot(), cx)
3817 });
3818 cx.run_until_parked();
3819
3820 let multibuffer = cx.new(|cx| {
3821 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
3822 multibuffer.set_excerpts_for_path(
3823 PathKey::sorted(0),
3824 buffer_1.clone(),
3825 [Point::zero()..buffer_1.read(cx).max_point()],
3826 0,
3827 cx,
3828 );
3829 multibuffer.add_diff(diff_1.clone(), cx);
3830 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
3831 multibuffer
3832 });
3833
3834 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
3835 (multibuffer.snapshot(cx), multibuffer.subscribe())
3836 });
3837
3838 assert_new_snapshot(
3839 &multibuffer,
3840 &mut snapshot,
3841 &mut subscription,
3842 cx,
3843 indoc!(
3844 "
3845 one
3846 - two
3847 "
3848 ),
3849 );
3850
3851 assert_eq!(snapshot.max_point(), Point::new(2, 0));
3852 assert_eq!(snapshot.len().0, 8);
3853
3854 assert_eq!(
3855 snapshot
3856 .dimensions_from_points::<Point>([Point::new(2, 0)])
3857 .collect::<Vec<_>>(),
3858 vec![Point::new(2, 0)]
3859 );
3860
3861 let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
3862 assert_eq!(translated_offset.0, "one\n".len());
3863 let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
3864 assert_eq!(translated_point, Point::new(1, 0));
3865
3866 // The same, for an excerpt that's not at the end of the multibuffer.
3867
3868 let text_2 = "foo\n".to_owned();
3869 let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
3870 multibuffer.update(cx, |multibuffer, cx| {
3871 multibuffer.set_excerpt_ranges_for_path(
3872 PathKey::sorted(1),
3873 buffer_2.clone(),
3874 &buffer_2.read(cx).snapshot(),
3875 vec![ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3876 cx,
3877 );
3878 });
3879
3880 assert_new_snapshot(
3881 &multibuffer,
3882 &mut snapshot,
3883 &mut subscription,
3884 cx,
3885 indoc!(
3886 "
3887 one
3888 - two
3889
3890 foo
3891 "
3892 ),
3893 );
3894
3895 assert_eq!(
3896 snapshot
3897 .dimensions_from_points::<Point>([Point::new(2, 0)])
3898 .collect::<Vec<_>>(),
3899 vec![Point::new(2, 0)]
3900 );
3901
3902 let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
3903 let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
3904 assert_eq!(buffer.remote_id(), buffer_1_id);
3905 assert_eq!(translated_offset.0, "one\n".len());
3906 let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
3907 assert_eq!(buffer.remote_id(), buffer_1_id);
3908 assert_eq!(translated_point, Point::new(1, 0));
3909}
3910
3911fn format_diff(
3912 text: &str,
3913 row_infos: &Vec<RowInfo>,
3914 boundary_rows: &HashSet<MultiBufferRow>,
3915 has_diff: Option<bool>,
3916) -> String {
3917 let has_diff =
3918 has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some()));
3919 text.split('\n')
3920 .enumerate()
3921 .zip(row_infos)
3922 .map(|((ix, line), info)| {
3923 let marker = match info.diff_status.map(|status| status.kind) {
3924 Some(DiffHunkStatusKind::Added) => "+ ",
3925 Some(DiffHunkStatusKind::Deleted) => "- ",
3926 Some(DiffHunkStatusKind::Modified) => unreachable!(),
3927 None => {
3928 if has_diff && !line.is_empty() {
3929 " "
3930 } else {
3931 ""
3932 }
3933 }
3934 };
3935 let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
3936 if has_diff {
3937 " ----------\n"
3938 } else {
3939 "---------\n"
3940 }
3941 } else {
3942 ""
3943 };
3944 let expand = info
3945 .expand_info
3946 .map(|expand_info| match expand_info.direction {
3947 ExpandExcerptDirection::Up => " [↑]",
3948 ExpandExcerptDirection::Down => " [↓]",
3949 ExpandExcerptDirection::UpAndDown => " [↕]",
3950 })
3951 .unwrap_or_default();
3952
3953 format!("{boundary_row}{marker}{line}{expand}")
3954 // let mbr = info
3955 // .multibuffer_row
3956 // .map(|row| format!("{:0>3}", row.0))
3957 // .unwrap_or_else(|| "???".to_string());
3958 // let byte_range = format!("{byte_range_start:0>3}..{byte_range_end:0>3}");
3959 // format!("{boundary_row}Row: {mbr}, Bytes: {byte_range} | {marker}{line}{expand}")
3960 })
3961 .collect::<Vec<_>>()
3962 .join("\n")
3963}
3964
3965// fn format_transforms(snapshot: &MultiBufferSnapshot) -> String {
3966// snapshot
3967// .diff_transforms
3968// .iter()
3969// .map(|transform| {
3970// let (kind, summary) = match transform {
3971// DiffTransform::DeletedHunk { summary, .. } => (" Deleted", (*summary).into()),
3972// DiffTransform::FilteredInsertedHunk { summary, .. } => (" Filtered", *summary),
3973// DiffTransform::InsertedHunk { summary, .. } => (" Inserted", *summary),
3974// DiffTransform::Unmodified { summary, .. } => ("Unmodified", *summary),
3975// };
3976// format!("{kind}(len: {}, lines: {:?})", summary.len, summary.lines)
3977// })
3978// .join("\n")
3979// }
3980
3981// fn format_excerpts(snapshot: &MultiBufferSnapshot) -> String {
3982// snapshot
3983// .excerpts
3984// .iter()
3985// .map(|excerpt| {
3986// format!(
3987// "Excerpt(buffer_range = {:?}, lines = {:?}, has_trailing_newline = {:?})",
3988// excerpt.range.context.to_point(&excerpt.buffer),
3989// excerpt.text_summary.lines,
3990// excerpt.has_trailing_newline
3991// )
3992// })
3993// .join("\n")
3994// }
3995
3996#[gpui::test]
3997async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
3998 let text = indoc!(
3999 "
4000 ZERO
4001 one
4002 TWO
4003 three
4004 six
4005 "
4006 );
4007 let base_text = indoc!(
4008 "
4009 one
4010 two
4011 three
4012 four
4013 five
4014 six
4015 "
4016 );
4017
4018 let buffer = cx.new(|cx| Buffer::local(text, cx));
4019 let diff = cx
4020 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
4021 cx.run_until_parked();
4022
4023 let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
4024
4025 let multibuffer = cx.new(|cx| {
4026 let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
4027 multibuffer.set_all_diff_hunks_expanded(cx);
4028 multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
4029 multibuffer
4030 });
4031
4032 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
4033 (multibuffer.snapshot(cx), multibuffer.subscribe())
4034 });
4035
4036 assert_eq!(snapshot.text(), base_text);
4037 assert_new_snapshot(
4038 &multibuffer,
4039 &mut snapshot,
4040 &mut subscription,
4041 cx,
4042 indoc!(
4043 "
4044 one
4045 - two
4046 three
4047 - four
4048 - five
4049 six
4050 "
4051 ),
4052 );
4053
4054 buffer.update(cx, |buffer, cx| {
4055 buffer.edit_via_marked_text(
4056 indoc!(
4057 "
4058 ZERO
4059 one
4060 «<inserted>»W«O
4061 T»hree
4062 six
4063 "
4064 ),
4065 None,
4066 cx,
4067 );
4068 });
4069 cx.run_until_parked();
4070 let update = diff
4071 .update(cx, |diff, cx| {
4072 diff.update_diff(
4073 buffer.read(cx).text_snapshot(),
4074 Some(base_text.into()),
4075 None,
4076 None,
4077 cx,
4078 )
4079 })
4080 .await;
4081 diff.update(cx, |diff, cx| {
4082 diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
4083 })
4084 .await;
4085 cx.run_until_parked();
4086
4087 assert_new_snapshot(
4088 &multibuffer,
4089 &mut snapshot,
4090 &mut subscription,
4091 cx,
4092 indoc! {
4093 "
4094 one
4095 - two
4096 - three
4097 - four
4098 - five
4099 six
4100 "
4101 },
4102 );
4103
4104 buffer.update(cx, |buffer, cx| {
4105 buffer.set_text("ZERO\nONE\nTWO\n", cx);
4106 });
4107 cx.run_until_parked();
4108 let update = diff
4109 .update(cx, |diff, cx| {
4110 diff.update_diff(
4111 buffer.read(cx).text_snapshot(),
4112 Some(base_text.into()),
4113 None,
4114 None,
4115 cx,
4116 )
4117 })
4118 .await;
4119 diff.update(cx, |diff, cx| {
4120 diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
4121 })
4122 .await;
4123 cx.run_until_parked();
4124
4125 assert_new_snapshot(
4126 &multibuffer,
4127 &mut snapshot,
4128 &mut subscription,
4129 cx,
4130 indoc! {
4131 "
4132 - one
4133 - two
4134 - three
4135 - four
4136 - five
4137 - six
4138 "
4139 },
4140 );
4141
4142 diff.update(cx, |diff, cx| {
4143 diff.set_base_text(
4144 Some("new base\n".into()),
4145 None,
4146 buffer.read(cx).text_snapshot(),
4147 cx,
4148 )
4149 })
4150 .await
4151 .unwrap();
4152 cx.run_until_parked();
4153
4154 assert_new_snapshot(
4155 &multibuffer,
4156 &mut snapshot,
4157 &mut subscription,
4158 cx,
4159 indoc! {"
4160 - new base
4161 "},
4162 );
4163}
4164
4165#[gpui::test]
4166async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) {
4167 let base_text = "aaa\nbbb\nccc\n";
4168 let text = "ddd\n";
4169 let buffer = cx.new(|cx| Buffer::local(text, cx));
4170 let diff = cx
4171 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
4172 cx.run_until_parked();
4173
4174 let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
4175
4176 let multibuffer = cx.new(|cx| {
4177 let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
4178 multibuffer.set_all_diff_hunks_expanded(cx);
4179 multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
4180 multibuffer
4181 });
4182
4183 let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
4184 (multibuffer.snapshot(cx), multibuffer.subscribe())
4185 });
4186
4187 assert_eq!(snapshot.text(), base_text);
4188 assert_new_snapshot(
4189 &multibuffer,
4190 &mut snapshot,
4191 &mut subscription,
4192 cx,
4193 indoc!(
4194 "
4195 - aaa
4196 - bbb
4197 - ccc
4198 "
4199 ),
4200 );
4201
4202 let update = diff
4203 .update(cx, |diff, cx| {
4204 diff.update_diff(
4205 buffer.read(cx).text_snapshot(),
4206 Some("ddd\n".into()),
4207 Some(true),
4208 None,
4209 cx,
4210 )
4211 })
4212 .await;
4213 diff.update(cx, |diff, cx| {
4214 diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
4215 })
4216 .detach();
4217
4218 let _hunks: Vec<_> = multibuffer
4219 .read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
4220 .diff_hunks()
4221 .collect();
4222}
4223
4224#[gpui::test]
4225async fn test_inverted_diff_secondary_version_mismatch(cx: &mut TestAppContext) {
4226 let base_text = "one\ntwo\nthree\nfour\nfive\n";
4227 let index_text = "one\nTWO\nthree\nfour\nfive\n";
4228 let buffer_text = "one\nTWO\nthree\nFOUR\nfive\n";
4229
4230 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
4231
4232 let unstaged_diff = cx
4233 .new(|cx| BufferDiff::new_with_base_text(index_text, &buffer.read(cx).text_snapshot(), cx));
4234 cx.run_until_parked();
4235
4236 let uncommitted_diff = cx.new(|cx| {
4237 let mut diff =
4238 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx);
4239 diff.set_secondary_diff(unstaged_diff.clone());
4240 diff
4241 });
4242 cx.run_until_parked();
4243
4244 buffer.update(cx, |buffer, cx| {
4245 buffer.edit([(0..0, "ZERO\n")], None, cx);
4246 });
4247
4248 let update = unstaged_diff
4249 .update(cx, |diff, cx| {
4250 diff.update_diff(
4251 buffer.read(cx).text_snapshot(),
4252 Some(index_text.into()),
4253 None,
4254 None,
4255 cx,
4256 )
4257 })
4258 .await;
4259 unstaged_diff
4260 .update(cx, |diff, cx| {
4261 diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
4262 })
4263 .await;
4264
4265 let base_text_buffer =
4266 uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
4267
4268 let multibuffer = cx.new(|cx| {
4269 let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
4270 multibuffer.set_all_diff_hunks_expanded(cx);
4271 multibuffer.add_inverted_diff(uncommitted_diff.clone(), buffer.clone(), cx);
4272 multibuffer
4273 });
4274
4275 let _hunks: Vec<_> = multibuffer
4276 .read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
4277 .diff_hunks()
4278 .collect();
4279}
4280
4281#[track_caller]
4282fn assert_excerpts_match(
4283 multibuffer: &Entity<MultiBuffer>,
4284 cx: &mut TestAppContext,
4285 expected: &str,
4286) {
4287 let mut output = String::new();
4288 multibuffer.read_with(cx, |multibuffer, cx| {
4289 for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() {
4290 output.push_str("-----\n");
4291 output.extend(buffer.text_for_range(range.context));
4292 if !output.ends_with('\n') {
4293 output.push('\n');
4294 }
4295 }
4296 });
4297 assert_eq!(output, expected);
4298}
4299
4300#[track_caller]
4301fn assert_new_snapshot(
4302 multibuffer: &Entity<MultiBuffer>,
4303 snapshot: &mut MultiBufferSnapshot,
4304 subscription: &mut Subscription<MultiBufferOffset>,
4305 cx: &mut TestAppContext,
4306 expected_diff: &str,
4307) {
4308 let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
4309 let actual_text = new_snapshot.text();
4310 let line_infos = new_snapshot
4311 .row_infos(MultiBufferRow(0))
4312 .collect::<Vec<_>>();
4313 let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None);
4314 pretty_assertions::assert_eq!(actual_diff, expected_diff);
4315 check_edits(
4316 snapshot,
4317 &new_snapshot,
4318 &subscription.consume().into_inner(),
4319 );
4320 *snapshot = new_snapshot;
4321}
4322
4323#[track_caller]
4324fn check_edits(
4325 old_snapshot: &MultiBufferSnapshot,
4326 new_snapshot: &MultiBufferSnapshot,
4327 edits: &[Edit<MultiBufferOffset>],
4328) {
4329 let mut text = old_snapshot.text();
4330 let new_text = new_snapshot.text();
4331 for edit in edits.iter().rev() {
4332 if !text.is_char_boundary(edit.old.start.0)
4333 || !text.is_char_boundary(edit.old.end.0)
4334 || !new_text.is_char_boundary(edit.new.start.0)
4335 || !new_text.is_char_boundary(edit.new.end.0)
4336 {
4337 panic!(
4338 "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}",
4339 edits, text, new_text
4340 );
4341 }
4342
4343 text.replace_range(
4344 edit.old.start.0..edit.old.end.0,
4345 &new_text[edit.new.start.0..edit.new.end.0],
4346 );
4347 }
4348
4349 pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits);
4350}
4351
4352#[track_caller]
4353fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) {
4354 let full_text = snapshot.text();
4355 for ix in 0..full_text.len() {
4356 let mut chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false);
4357 chunks.seek(MultiBufferOffset(ix)..snapshot.len());
4358 let tail = chunks.map(|chunk| chunk.text).collect::<String>();
4359 assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..);
4360 }
4361}
4362
4363#[track_caller]
4364fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) {
4365 let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
4366 for start_row in 1..all_line_numbers.len() {
4367 let line_numbers = snapshot
4368 .row_infos(MultiBufferRow(start_row as u32))
4369 .collect::<Vec<_>>();
4370 assert_eq!(
4371 line_numbers,
4372 all_line_numbers[start_row..],
4373 "start_row: {start_row}"
4374 );
4375 }
4376}
4377
4378#[track_caller]
4379fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
4380 let text = Rope::from(snapshot.text());
4381
4382 let mut left_anchors = Vec::new();
4383 let mut right_anchors = Vec::new();
4384 let mut offsets = Vec::new();
4385 let mut points = Vec::new();
4386 for offset in 0..=text.len() + 1 {
4387 let offset = MultiBufferOffset(offset);
4388 let clipped_left = snapshot.clip_offset(offset, Bias::Left);
4389 let clipped_right = snapshot.clip_offset(offset, Bias::Right);
4390 assert_eq!(
4391 clipped_left.0,
4392 text.clip_offset(offset.0, Bias::Left),
4393 "clip_offset({offset:?}, Left)"
4394 );
4395 assert_eq!(
4396 clipped_right.0,
4397 text.clip_offset(offset.0, Bias::Right),
4398 "clip_offset({offset:?}, Right)"
4399 );
4400 assert_eq!(
4401 snapshot.offset_to_point(clipped_left),
4402 text.offset_to_point(clipped_left.0),
4403 "offset_to_point({})",
4404 clipped_left.0
4405 );
4406 assert_eq!(
4407 snapshot.offset_to_point(clipped_right),
4408 text.offset_to_point(clipped_right.0),
4409 "offset_to_point({})",
4410 clipped_right.0
4411 );
4412 let anchor_after = snapshot.anchor_after(clipped_left);
4413 assert_eq!(
4414 anchor_after.to_offset(snapshot),
4415 clipped_left,
4416 "anchor_after({}).to_offset {anchor_after:?}",
4417 clipped_left.0
4418 );
4419 let anchor_before = snapshot.anchor_before(clipped_left);
4420 assert_eq!(
4421 anchor_before.to_offset(snapshot),
4422 clipped_left,
4423 "anchor_before({}).to_offset",
4424 clipped_left.0
4425 );
4426 left_anchors.push(anchor_before);
4427 right_anchors.push(anchor_after);
4428 offsets.push(clipped_left);
4429 points.push(text.offset_to_point(clipped_left.0));
4430 }
4431
4432 for row in 0..text.max_point().row {
4433 for column in 0..text.line_len(row) + 1 {
4434 let point = Point { row, column };
4435 let clipped_left = snapshot.clip_point(point, Bias::Left);
4436 let clipped_right = snapshot.clip_point(point, Bias::Right);
4437 assert_eq!(
4438 clipped_left,
4439 text.clip_point(point, Bias::Left),
4440 "clip_point({point:?}, Left)"
4441 );
4442 assert_eq!(
4443 clipped_right,
4444 text.clip_point(point, Bias::Right),
4445 "clip_point({point:?}, Right)"
4446 );
4447 assert_eq!(
4448 snapshot.point_to_offset(clipped_left).0,
4449 text.point_to_offset(clipped_left),
4450 "point_to_offset({clipped_left:?})"
4451 );
4452 assert_eq!(
4453 snapshot.point_to_offset(clipped_right).0,
4454 text.point_to_offset(clipped_right),
4455 "point_to_offset({clipped_right:?})"
4456 );
4457 }
4458 }
4459
4460 assert_eq!(
4461 snapshot.summaries_for_anchors::<MultiBufferOffset, _>(&left_anchors),
4462 offsets,
4463 "left_anchors <-> offsets"
4464 );
4465 assert_eq!(
4466 snapshot.summaries_for_anchors::<Point, _>(&left_anchors),
4467 points,
4468 "left_anchors <-> points"
4469 );
4470 assert_eq!(
4471 snapshot.summaries_for_anchors::<MultiBufferOffset, _>(&right_anchors),
4472 offsets,
4473 "right_anchors <-> offsets"
4474 );
4475 assert_eq!(
4476 snapshot.summaries_for_anchors::<Point, _>(&right_anchors),
4477 points,
4478 "right_anchors <-> points"
4479 );
4480
4481 for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] {
4482 for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() {
4483 if ix > 0 && *offset == MultiBufferOffset(252) && offset > &offsets[ix - 1] {
4484 let prev_anchor = left_anchors[ix - 1];
4485 assert!(
4486 anchor.cmp(&prev_anchor, snapshot).is_gt(),
4487 "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
4488 offsets[ix],
4489 offsets[ix - 1],
4490 );
4491 assert!(
4492 prev_anchor.cmp(anchor, snapshot).is_lt(),
4493 "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
4494 offsets[ix - 1],
4495 offsets[ix],
4496 );
4497 }
4498 }
4499 }
4500
4501 if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
4502 assert!(offset.0 <= buffer.len());
4503 }
4504 if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
4505 assert!(point <= buffer.max_point());
4506 }
4507}
4508
4509fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
4510 let max_row = snapshot.max_point().row;
4511 let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id();
4512 let text = text::Buffer::new(ReplicaId::LOCAL, buffer_id, snapshot.text());
4513 let mut line_indents = text
4514 .line_indents_in_row_range(0..max_row + 1)
4515 .collect::<Vec<_>>();
4516 for start_row in 0..snapshot.max_point().row {
4517 pretty_assertions::assert_eq!(
4518 snapshot
4519 .line_indents(MultiBufferRow(start_row), |_| true)
4520 .map(|(row, indent, _)| (row.0, indent))
4521 .collect::<Vec<_>>(),
4522 &line_indents[(start_row as usize)..],
4523 "line_indents({start_row})"
4524 );
4525 }
4526
4527 line_indents.reverse();
4528 pretty_assertions::assert_eq!(
4529 snapshot
4530 .reversed_line_indents(MultiBufferRow(max_row), |_| true)
4531 .map(|(row, indent, _)| (row.0, indent))
4532 .collect::<Vec<_>>(),
4533 &line_indents[..],
4534 "reversed_line_indents({max_row})"
4535 );
4536}
4537
4538#[gpui::test]
4539fn test_new_empty_buffer_uses_untitled_title(cx: &mut App) {
4540 let buffer = cx.new(|cx| Buffer::local("", cx));
4541 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4542
4543 assert_eq!(multibuffer.read(cx).title(cx), "untitled");
4544}
4545
4546#[gpui::test]
4547fn test_new_empty_buffer_uses_untitled_title_when_only_contains_whitespace(cx: &mut App) {
4548 let buffer = cx.new(|cx| Buffer::local("\n ", cx));
4549 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4550
4551 assert_eq!(multibuffer.read(cx).title(cx), "untitled");
4552}
4553
4554#[gpui::test]
4555fn test_new_empty_buffer_takes_first_line_for_title(cx: &mut App) {
4556 let buffer = cx.new(|cx| Buffer::local("Hello World\nSecond line", cx));
4557 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4558
4559 assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
4560}
4561
4562#[gpui::test]
4563fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) {
4564 let buffer = cx.new(|cx| Buffer::local("\nHello, World ", cx));
4565 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4566
4567 assert_eq!(multibuffer.read(cx).title(cx), "Hello, World");
4568}
4569
4570#[gpui::test]
4571fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) {
4572 let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
4573 let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
4574 let buffer = cx.new(|cx| Buffer::local(title, cx));
4575 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4576
4577 assert_eq!(multibuffer.read(cx).title(cx), title_after);
4578}
4579
4580#[gpui::test]
4581fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces(
4582 cx: &mut App,
4583) {
4584 let title = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddddeeeeeeeeee";
4585 let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd";
4586 let buffer = cx.new(|cx| Buffer::local(title, cx));
4587 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4588
4589 assert_eq!(multibuffer.read(cx).title(cx), title_after);
4590}
4591
4592#[gpui::test]
4593fn test_new_empty_buffers_title_can_be_set(cx: &mut App) {
4594 let buffer = cx.new(|cx| Buffer::local("Hello World", cx));
4595 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
4596 assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
4597
4598 multibuffer.update(cx, |multibuffer, cx| {
4599 multibuffer.set_title("Hey".into(), cx)
4600 });
4601 assert_eq!(multibuffer.read(cx).title(cx), "Hey");
4602}
4603
4604#[gpui::test(iterations = 100)]
4605fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
4606 let multibuffer = if rng.random() {
4607 let len = rng.random_range(0..10000);
4608 let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
4609 let buffer = cx.new(|cx| Buffer::local(text, cx));
4610 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
4611 } else {
4612 MultiBuffer::build_random(&mut rng, cx)
4613 };
4614
4615 let snapshot = multibuffer.read(cx).snapshot(cx);
4616
4617 let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false);
4618
4619 for chunk in chunks {
4620 let chunk_text = chunk.text;
4621 let chars_bitmap = chunk.chars;
4622 let tabs_bitmap = chunk.tabs;
4623
4624 if chunk_text.is_empty() {
4625 assert_eq!(
4626 chars_bitmap, 0,
4627 "Empty chunk should have empty chars bitmap"
4628 );
4629 assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
4630 continue;
4631 }
4632
4633 assert!(
4634 chunk_text.len() <= 128,
4635 "Chunk text length {} exceeds 128 bytes",
4636 chunk_text.len()
4637 );
4638
4639 // Verify chars bitmap
4640 let char_indices = chunk_text
4641 .char_indices()
4642 .map(|(i, _)| i)
4643 .collect::<Vec<_>>();
4644
4645 for byte_idx in 0..chunk_text.len() {
4646 let should_have_bit = char_indices.contains(&byte_idx);
4647 let has_bit = chars_bitmap & (1 << byte_idx) != 0;
4648
4649 if has_bit != should_have_bit {
4650 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
4651 eprintln!("Char indices: {:?}", char_indices);
4652 eprintln!("Chars bitmap: {:#b}", chars_bitmap);
4653 }
4654
4655 assert_eq!(
4656 has_bit, should_have_bit,
4657 "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
4658 byte_idx, chunk_text, should_have_bit, has_bit
4659 );
4660 }
4661
4662 for (byte_idx, byte) in chunk_text.bytes().enumerate() {
4663 let is_tab = byte == b'\t';
4664 let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
4665
4666 if has_bit != is_tab {
4667 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
4668 eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
4669 assert_eq!(
4670 has_bit, is_tab,
4671 "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
4672 byte_idx, chunk_text, byte as char, is_tab, has_bit
4673 );
4674 }
4675 }
4676 }
4677}
4678
4679#[gpui::test(iterations = 10)]
4680fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
4681 let settings_store = SettingsStore::test(cx);
4682 cx.set_global(settings_store);
4683 use buffer_diff::BufferDiff;
4684 use util::RandomCharIter;
4685
4686 let multibuffer = if rng.random() {
4687 let len = rng.random_range(100..10000);
4688 let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
4689 let buffer = cx.new(|cx| Buffer::local(text, cx));
4690 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
4691 } else {
4692 MultiBuffer::build_random(&mut rng, cx)
4693 };
4694
4695 let _diff_count = rng.random_range(1..5);
4696 let mut diffs = Vec::new();
4697
4698 multibuffer.update(cx, |multibuffer, cx| {
4699 for buffer_id in multibuffer.excerpt_buffer_ids() {
4700 if rng.random_bool(0.7) {
4701 if let Some(buffer_handle) = multibuffer.buffer(buffer_id) {
4702 let buffer_text = buffer_handle.read(cx).text();
4703 let mut base_text = String::new();
4704
4705 for line in buffer_text.lines() {
4706 if rng.random_bool(0.3) {
4707 continue;
4708 } else if rng.random_bool(0.3) {
4709 let line_len = rng.random_range(0..50);
4710 let modified_line = RandomCharIter::new(&mut rng)
4711 .take(line_len)
4712 .collect::<String>();
4713 base_text.push_str(&modified_line);
4714 base_text.push('\n');
4715 } else {
4716 base_text.push_str(line);
4717 base_text.push('\n');
4718 }
4719 }
4720
4721 if rng.random_bool(0.5) {
4722 let extra_lines = rng.random_range(1..5);
4723 for _ in 0..extra_lines {
4724 let line_len = rng.random_range(0..50);
4725 let extra_line = RandomCharIter::new(&mut rng)
4726 .take(line_len)
4727 .collect::<String>();
4728 base_text.push_str(&extra_line);
4729 base_text.push('\n');
4730 }
4731 }
4732
4733 let diff = cx.new(|cx| {
4734 BufferDiff::new_with_base_text(
4735 &base_text,
4736 &buffer_handle.read(cx).text_snapshot(),
4737 cx,
4738 )
4739 });
4740 diffs.push(diff.clone());
4741 multibuffer.add_diff(diff, cx);
4742 }
4743 }
4744 }
4745 });
4746
4747 multibuffer.update(cx, |multibuffer, cx| {
4748 if rng.random_bool(0.5) {
4749 multibuffer.set_all_diff_hunks_expanded(cx);
4750 } else {
4751 let snapshot = multibuffer.snapshot(cx);
4752 let text = snapshot.text();
4753
4754 let mut ranges = Vec::new();
4755 for _ in 0..rng.random_range(1..5) {
4756 if snapshot.len().0 == 0 {
4757 break;
4758 }
4759
4760 let diff_size = rng.random_range(5..1000);
4761 let mut start = rng.random_range(0..snapshot.len().0);
4762
4763 while !text.is_char_boundary(start) {
4764 start = start.saturating_sub(1);
4765 }
4766
4767 let mut end = rng.random_range(start..snapshot.len().0.min(start + diff_size));
4768
4769 while !text.is_char_boundary(end) {
4770 end = end.saturating_add(1);
4771 }
4772 let start_anchor = snapshot.anchor_after(MultiBufferOffset(start));
4773 let end_anchor = snapshot.anchor_before(MultiBufferOffset(end));
4774 ranges.push(start_anchor..end_anchor);
4775 }
4776 multibuffer.expand_diff_hunks(ranges, cx);
4777 }
4778 });
4779
4780 let snapshot = multibuffer.read(cx).snapshot(cx);
4781
4782 let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false);
4783
4784 for chunk in chunks {
4785 let chunk_text = chunk.text;
4786 let chars_bitmap = chunk.chars;
4787 let tabs_bitmap = chunk.tabs;
4788
4789 if chunk_text.is_empty() {
4790 assert_eq!(
4791 chars_bitmap, 0,
4792 "Empty chunk should have empty chars bitmap"
4793 );
4794 assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
4795 continue;
4796 }
4797
4798 assert!(
4799 chunk_text.len() <= 128,
4800 "Chunk text length {} exceeds 128 bytes",
4801 chunk_text.len()
4802 );
4803
4804 let char_indices = chunk_text
4805 .char_indices()
4806 .map(|(i, _)| i)
4807 .collect::<Vec<_>>();
4808
4809 for byte_idx in 0..chunk_text.len() {
4810 let should_have_bit = char_indices.contains(&byte_idx);
4811 let has_bit = chars_bitmap & (1 << byte_idx) != 0;
4812
4813 if has_bit != should_have_bit {
4814 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
4815 eprintln!("Char indices: {:?}", char_indices);
4816 eprintln!("Chars bitmap: {:#b}", chars_bitmap);
4817 }
4818
4819 assert_eq!(
4820 has_bit, should_have_bit,
4821 "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
4822 byte_idx, chunk_text, should_have_bit, has_bit
4823 );
4824 }
4825
4826 for (byte_idx, byte) in chunk_text.bytes().enumerate() {
4827 let is_tab = byte == b'\t';
4828 let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
4829
4830 if has_bit != is_tab {
4831 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
4832 eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
4833 assert_eq!(
4834 has_bit, is_tab,
4835 "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
4836 byte_idx, chunk_text, byte as char, is_tab, has_bit
4837 );
4838 }
4839 }
4840 }
4841}
4842
4843fn collect_word_diffs(
4844 base_text: &str,
4845 modified_text: &str,
4846 cx: &mut TestAppContext,
4847) -> Vec<String> {
4848 let buffer = cx.new(|cx| Buffer::local(modified_text, cx));
4849 let diff = cx
4850 .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
4851 cx.run_until_parked();
4852
4853 let multibuffer = cx.new(|cx| {
4854 let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
4855 multibuffer.add_diff(diff.clone(), cx);
4856 multibuffer
4857 });
4858
4859 multibuffer.update(cx, |multibuffer, cx| {
4860 multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
4861 });
4862
4863 let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
4864 let text = snapshot.text();
4865
4866 snapshot
4867 .diff_hunks()
4868 .flat_map(|hunk| hunk.word_diffs)
4869 .map(|range| text[range.start.0..range.end.0].to_string())
4870 .collect()
4871}
4872
4873#[gpui::test]
4874async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) {
4875 let settings_store = cx.update(|cx| SettingsStore::test(cx));
4876 cx.set_global(settings_store);
4877
4878 let base_text = "hello world foo bar\n";
4879 let modified_text = "hello WORLD foo BAR\n";
4880
4881 let word_diffs = collect_word_diffs(base_text, modified_text, cx);
4882
4883 assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]);
4884}
4885
4886#[gpui::test]
4887async fn test_word_diff_white_space(cx: &mut TestAppContext) {
4888 let settings_store = cx.update(|cx| SettingsStore::test(cx));
4889 cx.set_global(settings_store);
4890
4891 let base_text = "hello world foo bar\n";
4892 let modified_text = " hello world foo bar\n";
4893
4894 let word_diffs = collect_word_diffs(base_text, modified_text, cx);
4895
4896 assert_eq!(word_diffs, vec![" "]);
4897}
4898
4899#[gpui::test]
4900async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) {
4901 let settings_store = cx.update(|cx| SettingsStore::test(cx));
4902 cx.set_global(settings_store);
4903
4904 let base_text = "aaa bbb\nccc ddd\n";
4905 let modified_text = "aaa BBB\nccc DDD\n";
4906
4907 let word_diffs = collect_word_diffs(base_text, modified_text, cx);
4908
4909 assert_eq!(
4910 word_diffs,
4911 vec!["bbb", "ddd", "BBB", "DDD"],
4912 "consecutive modified lines should produce word diffs when line counts match"
4913 );
4914}
4915
4916#[gpui::test]
4917async fn test_word_diff_modified_lines_with_deletion_between(cx: &mut TestAppContext) {
4918 let settings_store = cx.update(|cx| SettingsStore::test(cx));
4919 cx.set_global(settings_store);
4920
4921 let base_text = "aaa bbb\ndeleted line\nccc ddd\n";
4922 let modified_text = "aaa BBB\nccc DDD\n";
4923
4924 let word_diffs = collect_word_diffs(base_text, modified_text, cx);
4925
4926 assert_eq!(
4927 word_diffs,
4928 Vec::<String>::new(),
4929 "modified lines with a deleted line between should not produce word diffs"
4930 );
4931}
4932
4933#[gpui::test]
4934async fn test_word_diff_disabled(cx: &mut TestAppContext) {
4935 let settings_store = cx.update(|cx| {
4936 let mut settings_store = SettingsStore::test(cx);
4937 settings_store.update_user_settings(cx, |settings| {
4938 settings.project.all_languages.defaults.word_diff_enabled = Some(false);
4939 });
4940 settings_store
4941 });
4942 cx.set_global(settings_store);
4943
4944 let base_text = "hello world\n";
4945 let modified_text = "hello WORLD\n";
4946
4947 let word_diffs = collect_word_diffs(base_text, modified_text, cx);
4948
4949 assert_eq!(
4950 word_diffs,
4951 Vec::<String>::new(),
4952 "word diffs should be empty when disabled"
4953 );
4954}
4955
4956/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts)
4957#[gpui::test]
4958fn test_excerpts_containment_functions(cx: &mut App) {
4959 // Multibuffer content for these tests:
4960 // 0123
4961 // 0: aa0
4962 // 1: aa1
4963 // -----
4964 // 2: bb0
4965 // 3: bb1
4966 // -----MultiBufferOffset(0)..
4967 // 4: cc0
4968
4969 let buffer_1 = cx.new(|cx| Buffer::local("aa0\naa1", cx));
4970 let buffer_2 = cx.new(|cx| Buffer::local("bb0\nbb1", cx));
4971 let buffer_3 = cx.new(|cx| Buffer::local("cc0", cx));
4972
4973 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
4974
4975 let (excerpt_1_id, excerpt_2_id, excerpt_3_id) = multibuffer.update(cx, |multibuffer, cx| {
4976 multibuffer.set_excerpts_for_path(
4977 PathKey::sorted(0),
4978 buffer_1.clone(),
4979 [Point::new(0, 0)..Point::new(1, 3)],
4980 0,
4981 cx,
4982 );
4983
4984 multibuffer.set_excerpts_for_path(
4985 PathKey::sorted(1),
4986 buffer_2.clone(),
4987 [Point::new(0, 0)..Point::new(1, 3)],
4988 0,
4989 cx,
4990 );
4991
4992 multibuffer.set_excerpts_for_path(
4993 PathKey::sorted(2),
4994 buffer_3.clone(),
4995 [Point::new(0, 0)..Point::new(0, 3)],
4996 0,
4997 cx,
4998 );
4999
5000 let mut ids = multibuffer.excerpt_ids().into_iter();
5001 (
5002 ids.next().unwrap(),
5003 ids.next().unwrap(),
5004 ids.next().unwrap(),
5005 )
5006 });
5007
5008 let snapshot = multibuffer.read(cx).snapshot(cx);
5009
5010 assert_eq!(snapshot.text(), "aa0\naa1\nbb0\nbb1\ncc0");
5011
5012 //// Test `excerpts_for_range`
5013
5014 let p00 = snapshot.point_to_offset(Point::new(0, 0));
5015 let p10 = snapshot.point_to_offset(Point::new(1, 0));
5016 let p20 = snapshot.point_to_offset(Point::new(2, 0));
5017 let p23 = snapshot.point_to_offset(Point::new(2, 3));
5018 let p13 = snapshot.point_to_offset(Point::new(1, 3));
5019 let p40 = snapshot.point_to_offset(Point::new(4, 0));
5020 let p43 = snapshot.point_to_offset(Point::new(4, 3));
5021
5022 let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p00).collect();
5023 assert_eq!(excerpts.len(), 1);
5024 assert_eq!(excerpts[0].id, excerpt_1_id);
5025
5026 // Cursor at very end of excerpt 3
5027 let excerpts: Vec<_> = snapshot.excerpts_for_range(p43..p43).collect();
5028 assert_eq!(excerpts.len(), 1);
5029 assert_eq!(excerpts[0].id, excerpt_3_id);
5030
5031 let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p23).collect();
5032 assert_eq!(excerpts.len(), 2);
5033 assert_eq!(excerpts[0].id, excerpt_1_id);
5034 assert_eq!(excerpts[1].id, excerpt_2_id);
5035
5036 // This range represent an selection with end-point just inside excerpt_2
5037 // Today we only expand the first excerpt, but another interpretation that
5038 // we could consider is expanding both here
5039 let excerpts: Vec<_> = snapshot.excerpts_for_range(p10..p20).collect();
5040 assert_eq!(excerpts.len(), 1);
5041 assert_eq!(excerpts[0].id, excerpt_1_id);
5042
5043 //// Test that `excerpts_for_range` and `excerpt_containing` agree for all single offsets (cursor positions)
5044 for offset in 0..=snapshot.len().0 {
5045 let offset = MultiBufferOffset(offset);
5046 let excerpts_for_range: Vec<_> = snapshot.excerpts_for_range(offset..offset).collect();
5047 assert_eq!(
5048 excerpts_for_range.len(),
5049 1,
5050 "Expected exactly one excerpt for offset {offset}",
5051 );
5052
5053 let excerpt_containing = snapshot.excerpt_containing(offset..offset);
5054 assert!(
5055 excerpt_containing.is_some(),
5056 "Expected excerpt_containing to find excerpt for offset {offset}",
5057 );
5058
5059 assert_eq!(
5060 excerpts_for_range[0].id,
5061 excerpt_containing.unwrap().id(),
5062 "excerpts_for_range and excerpt_containing should agree for offset {offset}",
5063 );
5064 }
5065
5066 //// Test `excerpt_containing` behavior with ranges:
5067
5068 // Ranges intersecting a single-excerpt
5069 let containing = snapshot.excerpt_containing(p00..p13);
5070 assert!(containing.is_some());
5071 assert_eq!(containing.unwrap().id(), excerpt_1_id);
5072
5073 // Ranges intersecting multiple excerpts (should return None)
5074 let containing = snapshot.excerpt_containing(p20..p40);
5075 assert!(
5076 containing.is_none(),
5077 "excerpt_containing should return None for ranges spanning multiple excerpts"
5078 );
5079}
5080
5081#[gpui::test]
5082fn test_range_to_buffer_ranges_with_range_bounds(cx: &mut App) {
5083 use std::ops::Bound;
5084
5085 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
5086 let buffer_2 = cx.new(|cx| Buffer::local("ccc", cx));
5087
5088 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
5089 let (excerpt_1_id, excerpt_2_id) = multibuffer.update(cx, |multibuffer, cx| {
5090 multibuffer.set_excerpts_for_path(
5091 PathKey::sorted(0),
5092 buffer_1.clone(),
5093 [Point::new(0, 0)..Point::new(1, 3)],
5094 0,
5095 cx,
5096 );
5097
5098 multibuffer.set_excerpts_for_path(
5099 PathKey::sorted(1),
5100 buffer_2.clone(),
5101 [Point::new(0, 0)..Point::new(0, 3)],
5102 0,
5103 cx,
5104 );
5105
5106 let excerpt_ids = multibuffer.excerpt_ids();
5107
5108 (excerpt_ids[0], excerpt_ids[1])
5109 });
5110
5111 let snapshot = multibuffer.read(cx).snapshot(cx);
5112 assert_eq!(snapshot.text(), "aaa\nbbb\nccc");
5113
5114 let excerpt_2_start = Point::new(2, 0);
5115
5116 let ranges_half_open = snapshot.range_to_buffer_ranges(Point::zero()..excerpt_2_start);
5117 assert_eq!(
5118 ranges_half_open.len(),
5119 1,
5120 "Half-open range ending at excerpt start should EXCLUDE that excerpt"
5121 );
5122 assert_eq!(ranges_half_open[0].2, excerpt_1_id);
5123
5124 let ranges_inclusive = snapshot.range_to_buffer_ranges(Point::zero()..=excerpt_2_start);
5125 assert_eq!(
5126 ranges_inclusive.len(),
5127 2,
5128 "Inclusive range ending at excerpt start should INCLUDE that excerpt"
5129 );
5130 assert_eq!(ranges_inclusive[0].2, excerpt_1_id);
5131 assert_eq!(ranges_inclusive[1].2, excerpt_2_id);
5132
5133 let ranges_unbounded =
5134 snapshot.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
5135 assert_eq!(
5136 ranges_unbounded.len(),
5137 2,
5138 "Unbounded end should include all excerpts"
5139 );
5140 assert_eq!(ranges_unbounded[0].2, excerpt_1_id);
5141 assert_eq!(ranges_unbounded[1].2, excerpt_2_id);
5142
5143 let ranges_excluded_end = snapshot.range_to_buffer_ranges((
5144 Bound::Included(Point::zero()),
5145 Bound::Excluded(excerpt_2_start),
5146 ));
5147 assert_eq!(
5148 ranges_excluded_end.len(),
5149 1,
5150 "Excluded end bound should exclude excerpt starting at that point"
5151 );
5152 assert_eq!(ranges_excluded_end[0].2, excerpt_1_id);
5153
5154 let buffer_empty = cx.new(|cx| Buffer::local("", cx));
5155 let multibuffer_trailing_empty = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
5156 let (te_excerpt_1_id, te_excerpt_2_id) =
5157 multibuffer_trailing_empty.update(cx, |multibuffer, cx| {
5158 multibuffer.set_excerpts_for_path(
5159 PathKey::sorted(0),
5160 buffer_1.clone(),
5161 [Point::new(0, 0)..Point::new(1, 3)],
5162 0,
5163 cx,
5164 );
5165
5166 multibuffer.set_excerpts_for_path(
5167 PathKey::sorted(1),
5168 buffer_empty.clone(),
5169 [Point::new(0, 0)..Point::new(0, 0)],
5170 0,
5171 cx,
5172 );
5173
5174 let excerpt_ids = multibuffer.excerpt_ids();
5175 (excerpt_ids[0], excerpt_ids[1])
5176 });
5177
5178 let snapshot_trailing = multibuffer_trailing_empty.read(cx).snapshot(cx);
5179 assert_eq!(snapshot_trailing.text(), "aaa\nbbb\n");
5180
5181 let max_point = snapshot_trailing.max_point();
5182
5183 let ranges_half_open_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..max_point);
5184 assert_eq!(
5185 ranges_half_open_max.len(),
5186 1,
5187 "Half-open range to max_point should EXCLUDE trailing empty excerpt at max_point"
5188 );
5189 assert_eq!(ranges_half_open_max[0].2, te_excerpt_1_id);
5190
5191 let ranges_inclusive_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..=max_point);
5192 assert_eq!(
5193 ranges_inclusive_max.len(),
5194 2,
5195 "Inclusive range to max_point should INCLUDE trailing empty excerpt"
5196 );
5197 assert_eq!(ranges_inclusive_max[0].2, te_excerpt_1_id);
5198 assert_eq!(ranges_inclusive_max[1].2, te_excerpt_2_id);
5199
5200 let ranges_unbounded_trailing = snapshot_trailing
5201 .range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
5202 assert_eq!(
5203 ranges_unbounded_trailing.len(),
5204 2,
5205 "Unbounded end should include trailing empty excerpt"
5206 );
5207 assert_eq!(ranges_unbounded_trailing[0].2, te_excerpt_1_id);
5208 assert_eq!(ranges_unbounded_trailing[1].2, te_excerpt_2_id);
5209}
5210
5211#[gpui::test]
5212fn test_cannot_seek_backward_after_excerpt_replacement(cx: &mut TestAppContext) {
5213 let buffer_b_text: String = (0..50).map(|i| format!("line_b {i}\n")).collect();
5214 let buffer_b = cx.new(|cx| Buffer::local(buffer_b_text, cx));
5215
5216 let buffer_c_text: String = (0..10).map(|i| format!("line_c {i}\n")).collect();
5217 let buffer_c = cx.new(|cx| Buffer::local(buffer_c_text, cx));
5218
5219 let buffer_d_text: String = (0..10).map(|i| format!("line_d {i}\n")).collect();
5220 let buffer_d = cx.new(|cx| Buffer::local(buffer_d_text, cx));
5221
5222 let path_b = PathKey::with_sort_prefix(0, rel_path("bbb.rs").into_arc());
5223 let path_c = PathKey::with_sort_prefix(0, rel_path("ddd.rs").into_arc());
5224 let path_d = PathKey::with_sort_prefix(0, rel_path("ccc.rs").into_arc());
5225
5226 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
5227
5228 multibuffer.update(cx, |multibuffer, cx| {
5229 multibuffer.set_excerpts_for_path(
5230 path_b.clone(),
5231 buffer_b.clone(),
5232 vec![
5233 Point::row_range(0..3),
5234 Point::row_range(15..18),
5235 Point::row_range(30..33),
5236 ],
5237 0,
5238 cx,
5239 );
5240 });
5241
5242 multibuffer.update(cx, |multibuffer, cx| {
5243 multibuffer.set_excerpts_for_path(
5244 path_c.clone(),
5245 buffer_c.clone(),
5246 vec![Point::row_range(0..3)],
5247 0,
5248 cx,
5249 );
5250 });
5251
5252 let (anchor_in_e_b2, anchor_in_e_b3) = multibuffer.read_with(cx, |multibuffer, cx| {
5253 let snapshot = multibuffer.snapshot(cx);
5254 let excerpt_ids: Vec<ExcerptId> = snapshot.excerpts().map(|(id, _, _)| id).collect();
5255 assert_eq!(excerpt_ids.len(), 4, "expected 4 excerpts (3×B + 1×C)");
5256
5257 let e_b2_id = excerpt_ids[1];
5258 let e_b3_id = excerpt_ids[2];
5259
5260 let e_b2 = snapshot.excerpt(e_b2_id).expect("E_B2 should exist");
5261 let e_b3 = snapshot.excerpt(e_b3_id).expect("E_B3 should exist");
5262
5263 let anchor_b2 = Anchor::in_buffer(e_b2_id, e_b2.range.context.start);
5264 let anchor_b3 = Anchor::in_buffer(e_b3_id, e_b3.range.context.start);
5265 (anchor_b2, anchor_b3)
5266 });
5267
5268 multibuffer.update(cx, |multibuffer, cx| {
5269 multibuffer.set_excerpts_for_path(
5270 path_b.clone(),
5271 buffer_b.clone(),
5272 vec![Point::row_range(0..3), Point::row_range(28..36)],
5273 0,
5274 cx,
5275 );
5276 });
5277
5278 multibuffer.update(cx, |multibuffer, cx| {
5279 multibuffer.set_excerpts_for_path(
5280 path_d.clone(),
5281 buffer_d.clone(),
5282 vec![Point::row_range(0..3)],
5283 0,
5284 cx,
5285 );
5286 });
5287
5288 multibuffer.read_with(cx, |multibuffer, cx| {
5289 let snapshot = multibuffer.snapshot(cx);
5290 snapshot.summaries_for_anchors::<Point, _>(&[anchor_in_e_b2, anchor_in_e_b3]);
5291 });
5292}