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