1use crate::{buffer, Buffer, Chunk};
2use collections::HashMap;
3use gpui::{AppContext, Entity, ModelContext, ModelHandle};
4use parking_lot::Mutex;
5use smallvec::{smallvec, SmallVec};
6use std::{cmp, iter, ops::Range};
7use sum_tree::{Bias, Cursor, SumTree};
8use text::{
9 subscription::{Subscription, Topic},
10 Anchor, AnchorRangeExt, Edit, Point, TextSummary,
11};
12use theme::SyntaxTheme;
13
14const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
15
16pub trait ToOffset {
17 fn to_offset<'a>(&self, content: &Snapshot) -> usize;
18}
19
20pub type ExcerptId = Location;
21
22#[derive(Default)]
23pub struct ExcerptList {
24 snapshot: Mutex<Snapshot>,
25 buffers: HashMap<usize, BufferState>,
26 subscriptions: Topic,
27}
28
29#[derive(Debug)]
30struct BufferState {
31 buffer: ModelHandle<Buffer>,
32 last_sync: clock::Global,
33 excerpts: Vec<ExcerptId>,
34}
35
36#[derive(Clone, Default)]
37pub struct Snapshot {
38 excerpts: SumTree<Excerpt>,
39}
40
41pub struct ExcerptProperties<'a, T> {
42 buffer: &'a ModelHandle<Buffer>,
43 range: Range<T>,
44 header_height: u8,
45}
46
47#[derive(Clone)]
48struct Excerpt {
49 id: ExcerptId,
50 buffer: buffer::BufferSnapshot,
51 range: Range<Anchor>,
52 text_summary: TextSummary,
53 header_height: u8,
54}
55
56#[derive(Clone, Debug, Default)]
57struct EntrySummary {
58 excerpt_id: ExcerptId,
59 text: TextSummary,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
63pub struct Location(SmallVec<[u8; 4]>);
64
65pub struct Chunks<'a> {
66 range: Range<usize>,
67 cursor: Cursor<'a, Excerpt, usize>,
68 header_height: u8,
69 entry_chunks: Option<buffer::BufferChunks<'a>>,
70 theme: Option<&'a SyntaxTheme>,
71}
72
73impl ExcerptList {
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn snapshot(&self, cx: &AppContext) -> Snapshot {
79 self.sync(cx);
80 self.snapshot.lock().clone()
81 }
82
83 pub fn subscribe(&mut self) -> Subscription {
84 self.subscriptions.subscribe()
85 }
86
87 pub fn push<O>(&mut self, props: ExcerptProperties<O>, cx: &mut ModelContext<Self>) -> ExcerptId
88 where
89 O: text::ToOffset,
90 {
91 self.sync(cx);
92
93 let buffer = props.buffer.read(cx);
94 let range = buffer.anchor_before(props.range.start)..buffer.anchor_after(props.range.end);
95 let mut snapshot = self.snapshot.lock();
96 let prev_id = snapshot.excerpts.last().map(|e| &e.id);
97 let id = ExcerptId::between(prev_id.unwrap_or(&ExcerptId::min()), &ExcerptId::max());
98
99 let edit_start = snapshot.excerpts.summary().text.bytes;
100 let excerpt = Excerpt::new(id.clone(), buffer.snapshot(), range, props.header_height);
101 let edit = Edit {
102 old: edit_start..edit_start,
103 new: edit_start..edit_start + excerpt.text_summary.bytes,
104 };
105 snapshot.excerpts.push(excerpt, &());
106 self.buffers
107 .entry(props.buffer.id())
108 .or_insert_with(|| BufferState {
109 buffer: props.buffer.clone(),
110 last_sync: buffer.version(),
111 excerpts: Default::default(),
112 })
113 .excerpts
114 .push(id.clone());
115
116 self.subscriptions.publish_mut([edit]);
117
118 id
119 }
120
121 fn sync(&self, cx: &AppContext) {
122 let mut snapshot = self.snapshot.lock();
123 let mut excerpts_to_edit = Vec::new();
124 for buffer_state in self.buffers.values() {
125 if buffer_state
126 .buffer
127 .read(cx)
128 .version()
129 .gt(&buffer_state.last_sync)
130 {
131 excerpts_to_edit.extend(
132 buffer_state
133 .excerpts
134 .iter()
135 .map(|excerpt_id| (excerpt_id, buffer_state)),
136 );
137 }
138 }
139 excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _)| *excerpt_id);
140
141 let mut edits = Vec::new();
142 let mut new_excerpts = SumTree::new();
143 let mut cursor = snapshot.excerpts.cursor::<(ExcerptId, usize)>();
144
145 for (id, buffer_state) in excerpts_to_edit {
146 new_excerpts.push_tree(cursor.slice(id, Bias::Left, &()), &());
147 let old_excerpt = cursor.item().unwrap();
148 let buffer = buffer_state.buffer.read(cx);
149
150 edits.extend(
151 buffer
152 .edits_since_in_range::<usize>(
153 old_excerpt.buffer.version(),
154 old_excerpt.range.clone(),
155 )
156 .map(|mut edit| {
157 let excerpt_old_start =
158 cursor.start().1 + old_excerpt.header_height as usize;
159 let excerpt_new_start =
160 new_excerpts.summary().text.bytes + old_excerpt.header_height as usize;
161 edit.old.start += excerpt_old_start;
162 edit.old.end += excerpt_old_start;
163 edit.new.start += excerpt_new_start;
164 edit.new.end += excerpt_new_start;
165 edit
166 }),
167 );
168
169 new_excerpts.push(
170 Excerpt::new(
171 id.clone(),
172 buffer.snapshot(),
173 old_excerpt.range.clone(),
174 old_excerpt.header_height,
175 ),
176 &(),
177 );
178
179 cursor.next(&());
180 }
181 new_excerpts.push_tree(cursor.suffix(&()), &());
182
183 drop(cursor);
184 snapshot.excerpts = new_excerpts;
185
186 self.subscriptions.publish(edits);
187 }
188}
189
190impl Entity for ExcerptList {
191 type Event = ();
192}
193
194impl Snapshot {
195 pub fn text(&self) -> String {
196 self.chunks(0..self.len(), None)
197 .map(|chunk| chunk.text)
198 .collect()
199 }
200
201 pub fn text_for_range<'a, T: ToOffset>(
202 &'a self,
203 range: Range<T>,
204 ) -> impl Iterator<Item = &'a str> {
205 self.chunks(range, None).map(|chunk| chunk.text)
206 }
207
208 pub fn len(&self) -> usize {
209 self.excerpts.summary().text.bytes
210 }
211
212 pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
213 let mut cursor = self.excerpts.cursor::<usize>();
214 cursor.seek(&offset, Bias::Right, &());
215 if let Some(excerpt) = cursor.item() {
216 let overshoot = offset - cursor.start();
217 let header_height = excerpt.header_height as usize;
218 if overshoot < header_height {
219 *cursor.start()
220 } else {
221 let excerpt_start =
222 text::ToOffset::to_offset(&excerpt.range.start, &excerpt.buffer);
223 let buffer_offset = excerpt.buffer.clip_offset(
224 excerpt_start + (offset - header_height - cursor.start()),
225 bias,
226 );
227 let offset_in_excerpt = if buffer_offset > excerpt_start {
228 buffer_offset - excerpt_start
229 } else {
230 0
231 };
232 cursor.start() + header_height + offset_in_excerpt
233 }
234 } else {
235 self.excerpts.summary().text.bytes
236 }
237 }
238
239 pub fn to_point(&self, offset: usize) -> Point {
240 let mut cursor = self.excerpts.cursor::<(usize, Point)>();
241 cursor.seek(&offset, Bias::Right, &());
242 if let Some(excerpt) = cursor.item() {
243 let overshoot = offset - cursor.start().0;
244 let header_height = excerpt.header_height as usize;
245 if overshoot < header_height {
246 cursor.start().1
247 } else {
248 let excerpt_start_offset =
249 text::ToOffset::to_offset(&excerpt.range.start, &excerpt.buffer);
250 let excerpt_start_point =
251 text::ToPoint::to_point(&excerpt.range.start, &excerpt.buffer);
252 let buffer_point = excerpt
253 .buffer
254 .to_point(excerpt_start_offset + (offset - header_height - cursor.start().0));
255 cursor.start().1
256 + Point::new(header_height as u32, 0)
257 + (buffer_point - excerpt_start_point)
258 }
259 } else {
260 self.excerpts.summary().text.lines
261 }
262 }
263
264 pub fn to_offset(&self, point: Point) -> usize {
265 let mut cursor = self.excerpts.cursor::<(Point, usize)>();
266 cursor.seek(&point, Bias::Right, &());
267 if let Some(excerpt) = cursor.item() {
268 let overshoot = point - cursor.start().0;
269 let header_height = Point::new(excerpt.header_height as u32, 0);
270 if overshoot < header_height {
271 cursor.start().1
272 } else {
273 let excerpt_start_offset =
274 text::ToOffset::to_offset(&excerpt.range.start, &excerpt.buffer);
275 let excerpt_start_point =
276 text::ToPoint::to_point(&excerpt.range.start, &excerpt.buffer);
277 let buffer_offset = excerpt
278 .buffer
279 .to_offset(excerpt_start_point + (point - header_height - cursor.start().0));
280 cursor.start().1
281 + excerpt.header_height as usize
282 + (buffer_offset - excerpt_start_offset)
283 }
284 } else {
285 self.excerpts.summary().text.bytes
286 }
287 }
288
289 pub fn chunks<'a, T: ToOffset>(
290 &'a self,
291 range: Range<T>,
292 theme: Option<&'a SyntaxTheme>,
293 ) -> Chunks<'a> {
294 let range = range.start.to_offset(self)..range.end.to_offset(self);
295 let mut cursor = self.excerpts.cursor::<usize>();
296 cursor.seek(&range.start, Bias::Right, &());
297
298 let mut header_height: u8 = 0;
299 let entry_chunks = cursor.item().map(|excerpt| {
300 let buffer_range = excerpt.range.to_offset(&excerpt.buffer);
301 header_height = excerpt.header_height;
302
303 let buffer_start;
304 let start_overshoot = range.start - cursor.start();
305 if start_overshoot < excerpt.header_height as usize {
306 header_height -= start_overshoot as u8;
307 buffer_start = buffer_range.start;
308 } else {
309 buffer_start =
310 buffer_range.start + start_overshoot - excerpt.header_height as usize;
311 header_height = 0;
312 }
313
314 let buffer_end;
315 let end_overshoot = range.end - cursor.start();
316 if end_overshoot < excerpt.header_height as usize {
317 header_height -= excerpt.header_height - end_overshoot as u8;
318 buffer_end = buffer_start;
319 } else {
320 buffer_end = cmp::min(
321 buffer_range.end,
322 buffer_range.start + end_overshoot - excerpt.header_height as usize,
323 );
324 }
325
326 excerpt.buffer.chunks(buffer_start..buffer_end, theme)
327 });
328
329 Chunks {
330 range,
331 cursor,
332 header_height,
333 entry_chunks,
334 theme,
335 }
336 }
337}
338
339impl Excerpt {
340 fn new(
341 id: ExcerptId,
342 buffer: buffer::BufferSnapshot,
343 range: Range<Anchor>,
344 header_height: u8,
345 ) -> Self {
346 let mut text_summary = buffer.text_summary_for_range::<TextSummary, _>(range.clone());
347 if header_height > 0 {
348 text_summary.first_line_chars = 0;
349 text_summary.lines.row += header_height as u32;
350 text_summary.lines_utf16.row += header_height as u32;
351 text_summary.bytes += header_height as usize;
352 text_summary.longest_row += header_height as u32;
353 }
354 text_summary.last_line_chars = 0;
355 text_summary.lines.row += 1;
356 text_summary.lines.column = 0;
357 text_summary.lines_utf16.row += 1;
358 text_summary.lines_utf16.column = 0;
359 text_summary.bytes += 1;
360
361 Excerpt {
362 id,
363 buffer,
364 range,
365 text_summary,
366 header_height,
367 }
368 }
369}
370
371impl sum_tree::Item for Excerpt {
372 type Summary = EntrySummary;
373
374 fn summary(&self) -> Self::Summary {
375 EntrySummary {
376 excerpt_id: self.id.clone(),
377 text: self.text_summary.clone(),
378 }
379 }
380}
381
382impl sum_tree::Summary for EntrySummary {
383 type Context = ();
384
385 fn add_summary(&mut self, summary: &Self, _: &()) {
386 debug_assert!(summary.excerpt_id > self.excerpt_id);
387 self.excerpt_id = summary.excerpt_id.clone();
388 self.text.add_summary(&summary.text, &());
389 }
390}
391
392impl<'a> sum_tree::Dimension<'a, EntrySummary> for usize {
393 fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
394 *self += summary.text.bytes;
395 }
396}
397
398impl<'a> sum_tree::Dimension<'a, EntrySummary> for Point {
399 fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
400 *self += summary.text.lines;
401 }
402}
403
404impl<'a> sum_tree::Dimension<'a, EntrySummary> for Location {
405 fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
406 debug_assert!(summary.excerpt_id > *self);
407 *self = summary.excerpt_id.clone();
408 }
409}
410
411impl<'a> Iterator for Chunks<'a> {
412 type Item = Chunk<'a>;
413
414 fn next(&mut self) -> Option<Self::Item> {
415 loop {
416 if self.header_height > 0 {
417 let chunk = Chunk {
418 text: unsafe {
419 std::str::from_utf8_unchecked(&NEWLINES[..self.header_height as usize])
420 },
421 ..Default::default()
422 };
423 self.header_height = 0;
424 return Some(chunk);
425 }
426
427 if let Some(entry_chunks) = self.entry_chunks.as_mut() {
428 if let Some(chunk) = entry_chunks.next() {
429 return Some(chunk);
430 }
431 self.entry_chunks.take();
432 if self.cursor.end(&()) <= self.range.end {
433 return Some(Chunk {
434 text: "\n",
435 ..Default::default()
436 });
437 }
438 }
439
440 self.cursor.next(&());
441 if *self.cursor.start() >= self.range.end {
442 return None;
443 }
444
445 let excerpt = self.cursor.item()?;
446 let buffer_range = excerpt.range.to_offset(&excerpt.buffer);
447
448 let buffer_end = cmp::min(
449 buffer_range.end,
450 buffer_range.start + self.range.end
451 - excerpt.header_height as usize
452 - self.cursor.start(),
453 );
454
455 self.header_height = excerpt.header_height;
456 self.entry_chunks = Some(
457 excerpt
458 .buffer
459 .chunks(buffer_range.start..buffer_end, self.theme),
460 );
461 }
462 }
463}
464
465impl ToOffset for usize {
466 fn to_offset<'a>(&self, _: &Snapshot) -> usize {
467 *self
468 }
469}
470
471impl ToOffset for Point {
472 fn to_offset<'a>(&self, snapshot: &Snapshot) -> usize {
473 snapshot.to_offset(*self)
474 }
475}
476
477impl Default for Location {
478 fn default() -> Self {
479 Self::min()
480 }
481}
482
483impl Location {
484 pub fn min() -> Self {
485 Self(smallvec![u8::MIN])
486 }
487
488 pub fn max() -> Self {
489 Self(smallvec![u8::MAX])
490 }
491
492 pub fn between(lhs: &Self, rhs: &Self) -> Self {
493 let lhs = lhs.0.iter().copied().chain(iter::repeat(u8::MIN));
494 let rhs = rhs.0.iter().copied().chain(iter::repeat(u8::MAX));
495 let mut location = SmallVec::new();
496 for (lhs, rhs) in lhs.zip(rhs) {
497 let mid = lhs + (rhs.saturating_sub(lhs)) / 2;
498 location.push(mid);
499 if mid > lhs {
500 break;
501 }
502 }
503 Self(location)
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::Buffer;
511 use gpui::MutableAppContext;
512 use rand::prelude::*;
513 use std::{env, mem};
514 use text::{Point, RandomCharIter};
515 use util::test::sample_text;
516
517 #[gpui::test]
518 fn test_excerpt_buffer(cx: &mut MutableAppContext) {
519 let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
520 let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
521
522 let list = cx.add_model(|_| ExcerptList::new());
523
524 let subscription = list.update(cx, |list, cx| {
525 let subscription = list.subscribe();
526 list.push(
527 ExcerptProperties {
528 buffer: &buffer_1,
529 range: Point::new(1, 2)..Point::new(2, 5),
530 header_height: 2,
531 },
532 cx,
533 );
534 assert_eq!(
535 subscription.consume().into_inner(),
536 [Edit {
537 old: 0..0,
538 new: 0..13
539 }]
540 );
541
542 list.push(
543 ExcerptProperties {
544 buffer: &buffer_1,
545 range: Point::new(3, 3)..Point::new(4, 4),
546 header_height: 1,
547 },
548 cx,
549 );
550 list.push(
551 ExcerptProperties {
552 buffer: &buffer_2,
553 range: Point::new(3, 1)..Point::new(3, 3),
554 header_height: 3,
555 },
556 cx,
557 );
558 assert_eq!(
559 subscription.consume().into_inner(),
560 [Edit {
561 old: 13..13,
562 new: 13..29
563 }]
564 );
565
566 subscription
567 });
568
569 assert_eq!(
570 list.read(cx).snapshot(cx).text(),
571 concat!(
572 "\n", // Preserve newlines
573 "\n", //
574 "bbbb\n", //
575 "ccccc\n", //
576 "\n", //
577 "ddd\n", //
578 "eeee\n", //
579 "\n", //
580 "\n", //
581 "\n", //
582 "jj\n" //
583 )
584 );
585
586 buffer_1.update(cx, |buffer, cx| {
587 buffer.edit(
588 [
589 Point::new(0, 0)..Point::new(0, 0),
590 Point::new(2, 1)..Point::new(2, 3),
591 ],
592 "\n",
593 cx,
594 );
595 });
596
597 assert_eq!(
598 list.read(cx).snapshot(cx).text(),
599 concat!(
600 "\n", // Preserve newlines
601 "\n", //
602 "bbbb\n", //
603 "c\n", //
604 "cc\n", //
605 "\n", //
606 "ddd\n", //
607 "eeee\n", //
608 "\n", //
609 "\n", //
610 "\n", //
611 "jj\n" //
612 )
613 );
614
615 assert_eq!(
616 subscription.consume().into_inner(),
617 [Edit {
618 old: 8..10,
619 new: 8..9
620 }]
621 );
622 }
623
624 #[gpui::test(iterations = 100)]
625 fn test_random_excerpts(cx: &mut MutableAppContext, mut rng: StdRng) {
626 let operations = env::var("OPERATIONS")
627 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
628 .unwrap_or(10);
629
630 let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
631 let list = cx.add_model(|_| ExcerptList::new());
632 let mut excerpt_ids = Vec::new();
633 let mut expected_excerpts = Vec::new();
634 let mut old_versions = Vec::new();
635
636 for _ in 0..operations {
637 match rng.gen_range(0..100) {
638 0..=19 if !buffers.is_empty() => {
639 let buffer = buffers.choose(&mut rng).unwrap();
640 buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 1, cx));
641 }
642 _ => {
643 let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
644 let base_text = RandomCharIter::new(&mut rng).take(10).collect::<String>();
645 buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
646 buffers.last().unwrap()
647 } else {
648 buffers.choose(&mut rng).unwrap()
649 };
650
651 let buffer = buffer_handle.read(cx);
652 let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
653 let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
654 let header_height = rng.gen_range(0..=5);
655 let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
656 log::info!(
657 "Pushing excerpt wih header {}, buffer {}: {:?}[{:?}] = {:?}",
658 header_height,
659 buffer_handle.id(),
660 buffer.text(),
661 start_ix..end_ix,
662 &buffer.text()[start_ix..end_ix]
663 );
664
665 let excerpt_id = list.update(cx, |list, cx| {
666 list.push(
667 ExcerptProperties {
668 buffer: &buffer_handle,
669 range: start_ix..end_ix,
670 header_height,
671 },
672 cx,
673 )
674 });
675 excerpt_ids.push(excerpt_id);
676 expected_excerpts.push((buffer_handle.clone(), anchor_range, header_height));
677 }
678 }
679
680 if rng.gen_bool(0.3) {
681 list.update(cx, |list, cx| {
682 old_versions.push((list.snapshot(cx), list.subscribe()));
683 })
684 }
685
686 let snapshot = list.read(cx).snapshot(cx);
687
688 let mut expected_text = String::new();
689 for (buffer, range, header_height) in &expected_excerpts {
690 let buffer_id = buffer.id();
691 let buffer = buffer.read(cx);
692 let buffer_range = range.to_offset(buffer);
693 let buffer_start_point = buffer.to_point(buffer_range.start);
694
695 for _ in 0..*header_height {
696 expected_text.push('\n');
697 }
698
699 let excerpt_start = TextSummary::from(expected_text.as_str());
700 expected_text.extend(buffer.text_for_range(buffer_range.clone()));
701 expected_text.push('\n');
702
703 for buffer_offset in buffer_range.clone() {
704 let offset = excerpt_start.bytes + (buffer_offset - buffer_range.start);
705 let left_offset = snapshot.clip_offset(offset, Bias::Left);
706 let right_offset = snapshot.clip_offset(offset, Bias::Right);
707 let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left);
708 let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right);
709 let left_point = snapshot.to_point(left_offset);
710
711 assert_eq!(
712 left_offset,
713 excerpt_start.bytes + (buffer_left_offset - buffer_range.start),
714 "clip_offset({}, Left). buffer: {}, buffer offset: {}",
715 offset,
716 buffer_id,
717 buffer_offset,
718 );
719 assert_eq!(
720 right_offset,
721 excerpt_start.bytes + (buffer_right_offset - buffer_range.start),
722 "clip_offset({}, Right). buffer: {}, buffer offset: {}",
723 offset,
724 buffer_id,
725 buffer_offset,
726 );
727 assert_eq!(
728 left_point,
729 excerpt_start.lines
730 + (buffer.to_point(buffer_left_offset) - buffer_start_point),
731 "to_point({}). buffer: {}, buffer offset: {}",
732 offset,
733 buffer_id,
734 buffer_offset,
735 );
736 assert_eq!(
737 snapshot.to_offset(left_point),
738 left_offset,
739 "to_offset({:?})",
740 left_point,
741 )
742 }
743 }
744
745 assert_eq!(snapshot.text(), expected_text);
746
747 for _ in 0..10 {
748 let end_ix = snapshot.clip_offset(rng.gen_range(0..=snapshot.len()), Bias::Right);
749 let start_ix = snapshot.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
750
751 assert_eq!(
752 snapshot
753 .text_for_range(start_ix..end_ix)
754 .collect::<String>(),
755 &expected_text[start_ix..end_ix],
756 "incorrect text for range {:?}",
757 start_ix..end_ix
758 );
759 }
760 }
761
762 let snapshot = list.read(cx).snapshot(cx);
763 for (old_snapshot, subscription) in old_versions {
764 let edits = subscription.consume().into_inner();
765
766 log::info!(
767 "applying edits since old text: {:?}: {:?}",
768 old_snapshot.text(),
769 edits,
770 );
771
772 let mut text = old_snapshot.text();
773 for edit in edits {
774 let new_text: String = snapshot.text_for_range(edit.new.clone()).collect();
775 text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text);
776 }
777 assert_eq!(text.to_string(), snapshot.text());
778 }
779 }
780
781 #[gpui::test(iterations = 100)]
782 fn test_location(mut rng: StdRng) {
783 let mut lhs = Default::default();
784 let mut rhs = Default::default();
785 while lhs == rhs {
786 lhs = Location(
787 (0..rng.gen_range(1..=5))
788 .map(|_| rng.gen_range(0..=100))
789 .collect(),
790 );
791 rhs = Location(
792 (0..rng.gen_range(1..=5))
793 .map(|_| rng.gen_range(0..=100))
794 .collect(),
795 );
796 }
797
798 if lhs > rhs {
799 mem::swap(&mut lhs, &mut rhs);
800 }
801
802 let middle = Location::between(&lhs, &rhs);
803 assert!(middle > lhs);
804 assert!(middle < rhs);
805 for ix in 0..middle.0.len() - 1 {
806 assert!(
807 middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0)
808 || middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0)
809 );
810 }
811 }
812}