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