1use super::{
2 fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
3 TextHighlights,
4};
5use crate::{MultiBufferSnapshot, ToPoint};
6use gpui::fonts::HighlightStyle;
7use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
8use parking_lot::Mutex;
9use std::{
10 cmp,
11 ops::{Add, AddAssign, Range, Sub},
12};
13use util::post_inc;
14
15pub type SuggestionEdit = Edit<SuggestionOffset>;
16
17#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
18pub struct SuggestionOffset(pub usize);
19
20impl Add for SuggestionOffset {
21 type Output = Self;
22
23 fn add(self, rhs: Self) -> Self::Output {
24 Self(self.0 + rhs.0)
25 }
26}
27
28impl Sub for SuggestionOffset {
29 type Output = Self;
30
31 fn sub(self, rhs: Self) -> Self::Output {
32 Self(self.0 - rhs.0)
33 }
34}
35
36impl AddAssign for SuggestionOffset {
37 fn add_assign(&mut self, rhs: Self) {
38 self.0 += rhs.0;
39 }
40}
41
42#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
43pub struct SuggestionPoint(pub Point);
44
45impl SuggestionPoint {
46 pub fn new(row: u32, column: u32) -> Self {
47 Self(Point::new(row, column))
48 }
49
50 pub fn row(self) -> u32 {
51 self.0.row
52 }
53
54 pub fn column(self) -> u32 {
55 self.0.column
56 }
57}
58
59
60#[derive(Clone, Debug)]
61pub struct Suggestion<T> {
62 pub position: T,
63 pub text: Rope,
64}
65
66pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
67
68impl SuggestionMap {
69 pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
70 let snapshot = SuggestionSnapshot {
71 fold_snapshot,
72 suggestion: None,
73 version: 0,
74 };
75 (Self(Mutex::new(snapshot.clone())), snapshot)
76 }
77
78 pub fn replace<T>(
79 &self,
80 new_suggestion: Option<Suggestion<T>>,
81 fold_snapshot: FoldSnapshot,
82 fold_edits: Vec<FoldEdit>,
83 ) -> (
84 SuggestionSnapshot,
85 Vec<SuggestionEdit>,
86 Option<Suggestion<FoldOffset>>,
87 )
88 where
89 T: ToPoint,
90 {
91 let new_suggestion = new_suggestion.map(|new_suggestion| {
92 let buffer_point = new_suggestion
93 .position
94 .to_point(fold_snapshot.buffer_snapshot());
95 let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
96 let fold_offset = fold_point.to_offset(&fold_snapshot);
97 Suggestion {
98 position: fold_offset,
99 text: new_suggestion.text,
100 }
101 });
102
103 let (_, edits) = self.sync(fold_snapshot, fold_edits);
104 let mut snapshot = self.0.lock();
105
106 let mut patch = Patch::new(edits);
107 let old_suggestion = snapshot.suggestion.take();
108 if let Some(suggestion) = &old_suggestion {
109 patch = patch.compose([SuggestionEdit {
110 old: SuggestionOffset(suggestion.position.0)
111 ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
112 new: SuggestionOffset(suggestion.position.0)
113 ..SuggestionOffset(suggestion.position.0),
114 }]);
115 }
116
117 if let Some(suggestion) = new_suggestion.as_ref() {
118 patch = patch.compose([SuggestionEdit {
119 old: SuggestionOffset(suggestion.position.0)
120 ..SuggestionOffset(suggestion.position.0),
121 new: SuggestionOffset(suggestion.position.0)
122 ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
123 }]);
124 }
125
126 snapshot.suggestion = new_suggestion;
127 snapshot.version += 1;
128 (snapshot.clone(), patch.into_inner(), old_suggestion)
129 }
130
131 pub fn sync(
132 &self,
133 fold_snapshot: FoldSnapshot,
134 fold_edits: Vec<FoldEdit>,
135 ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
136 let mut snapshot = self.0.lock();
137
138 if snapshot.fold_snapshot.version != fold_snapshot.version {
139 snapshot.version += 1;
140 }
141
142 let mut suggestion_edits = Vec::new();
143
144 let mut suggestion_old_len = 0;
145 let mut suggestion_new_len = 0;
146 for fold_edit in fold_edits {
147 let start = fold_edit.new.start;
148 let end = FoldOffset(start.0 + fold_edit.old_len().0);
149 if let Some(suggestion) = snapshot.suggestion.as_mut() {
150 if end <= suggestion.position {
151 suggestion.position.0 += fold_edit.new_len().0;
152 suggestion.position.0 -= fold_edit.old_len().0;
153 } else if start > suggestion.position {
154 suggestion_old_len = suggestion.text.len();
155 suggestion_new_len = suggestion_old_len;
156 } else {
157 suggestion_old_len = suggestion.text.len();
158 snapshot.suggestion.take();
159 suggestion_edits.push(SuggestionEdit {
160 old: SuggestionOffset(fold_edit.old.start.0)
161 ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
162 new: SuggestionOffset(fold_edit.new.start.0)
163 ..SuggestionOffset(fold_edit.new.end.0),
164 });
165 continue;
166 }
167 }
168
169 suggestion_edits.push(SuggestionEdit {
170 old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
171 ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
172 new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
173 ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
174 });
175 }
176 snapshot.fold_snapshot = fold_snapshot;
177
178 (snapshot.clone(), suggestion_edits)
179 }
180
181 pub fn has_suggestion(&self) -> bool {
182 let snapshot = self.0.lock();
183 snapshot.suggestion.is_some()
184 }
185}
186
187#[derive(Clone)]
188pub struct SuggestionSnapshot {
189 pub fold_snapshot: FoldSnapshot,
190 pub suggestion: Option<Suggestion<FoldOffset>>,
191 pub version: usize,
192}
193
194impl SuggestionSnapshot {
195 pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
196 self.fold_snapshot.buffer_snapshot()
197 }
198
199 pub fn max_point(&self) -> SuggestionPoint {
200 if let Some(suggestion) = self.suggestion.as_ref() {
201 let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
202 let mut max_point = suggestion_point.0;
203 max_point += suggestion.text.max_point();
204 max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
205 SuggestionPoint(max_point)
206 } else {
207 SuggestionPoint(self.fold_snapshot.max_point().0)
208 }
209 }
210
211 pub fn len(&self) -> SuggestionOffset {
212 if let Some(suggestion) = self.suggestion.as_ref() {
213 let mut len = suggestion.position.0;
214 len += suggestion.text.len();
215 len += self.fold_snapshot.len().0 - suggestion.position.0;
216 SuggestionOffset(len)
217 } else {
218 SuggestionOffset(self.fold_snapshot.len().0)
219 }
220 }
221
222 pub fn line_len(&self, row: u32) -> u32 {
223 if let Some(suggestion) = &self.suggestion {
224 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
225 let suggestion_end = suggestion_start + suggestion.text.max_point();
226
227 if row < suggestion_start.row {
228 self.fold_snapshot.line_len(row)
229 } else if row > suggestion_end.row {
230 self.fold_snapshot
231 .line_len(suggestion_start.row + (row - suggestion_end.row))
232 } else {
233 let mut result = suggestion.text.line_len(row - suggestion_start.row);
234 if row == suggestion_start.row {
235 result += suggestion_start.column;
236 }
237 if row == suggestion_end.row {
238 result +=
239 self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
240 }
241 result
242 }
243 } else {
244 self.fold_snapshot.line_len(row)
245 }
246 }
247
248 pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
249 if let Some(suggestion) = self.suggestion.as_ref() {
250 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
251 let suggestion_end = suggestion_start + suggestion.text.max_point();
252 if point.0 <= suggestion_start {
253 SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
254 } else if point.0 > suggestion_end {
255 let fold_point = self.fold_snapshot.clip_point(
256 FoldPoint(suggestion_start + (point.0 - suggestion_end)),
257 bias,
258 );
259 let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
260 if bias == Bias::Left && suggestion_point == suggestion_end {
261 SuggestionPoint(suggestion_start)
262 } else {
263 SuggestionPoint(suggestion_point)
264 }
265 } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
266 SuggestionPoint(suggestion_start)
267 } else {
268 let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
269 > suggestion_start.column
270 {
271 FoldPoint(suggestion_start + Point::new(0, 1))
272 } else {
273 FoldPoint(suggestion_start + Point::new(1, 0))
274 };
275 let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
276 SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
277 }
278 } else {
279 SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
280 }
281 }
282
283 pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
284 if let Some(suggestion) = self.suggestion.as_ref() {
285 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
286 let suggestion_end = suggestion_start + suggestion.text.max_point();
287
288 if point.0 <= suggestion_start {
289 SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
290 } else if point.0 > suggestion_end {
291 let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
292 .to_offset(&self.fold_snapshot);
293 SuggestionOffset(fold_offset.0 + suggestion.text.len())
294 } else {
295 let offset_in_suggestion =
296 suggestion.text.point_to_offset(point.0 - suggestion_start);
297 SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
298 }
299 } else {
300 SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
301 }
302 }
303
304 pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
305 if let Some(suggestion) = self.suggestion.as_ref() {
306 let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
307 if offset.0 <= suggestion.position.0 {
308 SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
309 } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
310 let fold_point = FoldOffset(offset.0 - suggestion.text.len())
311 .to_point(&self.fold_snapshot)
312 .0;
313
314 SuggestionPoint(
315 suggestion_point_start
316 + suggestion.text.max_point()
317 + (fold_point - suggestion_point_start),
318 )
319 } else {
320 let point_in_suggestion = suggestion
321 .text
322 .offset_to_point(offset.0 - suggestion.position.0);
323 SuggestionPoint(suggestion_point_start + point_in_suggestion)
324 }
325 } else {
326 SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
327 }
328 }
329
330 pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
331 if let Some(suggestion) = self.suggestion.as_ref() {
332 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
333 let suggestion_end = suggestion_start + suggestion.text.max_point();
334
335 if point.0 <= suggestion_start {
336 FoldPoint(point.0)
337 } else if point.0 > suggestion_end {
338 FoldPoint(suggestion_start + (point.0 - suggestion_end))
339 } else {
340 FoldPoint(suggestion_start)
341 }
342 } else {
343 FoldPoint(point.0)
344 }
345 }
346
347 pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
348 if let Some(suggestion) = self.suggestion.as_ref() {
349 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
350
351 if point.0 <= suggestion_start {
352 SuggestionPoint(point.0)
353 } else {
354 let suggestion_end = suggestion_start + suggestion.text.max_point();
355 SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
356 }
357 } else {
358 SuggestionPoint(point.0)
359 }
360 }
361
362 pub fn text_summary(&self) -> TextSummary {
363 self.text_summary_for_range(Default::default()..self.max_point())
364 }
365
366 pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
367 if let Some(suggestion) = self.suggestion.as_ref() {
368 let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
369 let suggestion_end = suggestion_start + suggestion.text.max_point();
370 let mut summary = TextSummary::default();
371
372 let prefix_range =
373 cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
374 if prefix_range.start < prefix_range.end {
375 summary += self.fold_snapshot.text_summary_for_range(
376 FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
377 );
378 }
379
380 let suggestion_range =
381 cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
382 if suggestion_range.start < suggestion_range.end {
383 let point_range = suggestion_range.start - suggestion_start
384 ..suggestion_range.end - suggestion_start;
385 let offset_range = suggestion.text.point_to_offset(point_range.start)
386 ..suggestion.text.point_to_offset(point_range.end);
387 summary += suggestion
388 .text
389 .cursor(offset_range.start)
390 .summary::<TextSummary>(offset_range.end);
391 }
392
393 let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
394 if suffix_range.start < suffix_range.end {
395 let start = suggestion_start + (suffix_range.start - suggestion_end);
396 let end = suggestion_start + (suffix_range.end - suggestion_end);
397 summary += self
398 .fold_snapshot
399 .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
400 }
401
402 summary
403 } else {
404 self.fold_snapshot
405 .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
406 }
407 }
408
409 pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
410 let start = self.to_offset(start);
411 self.chunks(start..self.len(), false, None, None)
412 .flat_map(|chunk| chunk.text.chars())
413 }
414
415 pub fn chunks<'a>(
416 &'a self,
417 range: Range<SuggestionOffset>,
418 language_aware: bool,
419 text_highlights: Option<&'a TextHighlights>,
420 suggestion_highlight: Option<HighlightStyle>,
421 ) -> SuggestionChunks<'a> {
422 if let Some(suggestion) = self.suggestion.as_ref() {
423 let suggestion_range =
424 suggestion.position.0..suggestion.position.0 + suggestion.text.len();
425
426 let prefix_chunks = if range.start.0 < suggestion_range.start {
427 Some(self.fold_snapshot.chunks(
428 FoldOffset(range.start.0)
429 ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
430 language_aware,
431 text_highlights,
432 ))
433 } else {
434 None
435 };
436
437 let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
438 ..cmp::min(range.end.0, suggestion_range.end);
439 let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
440 {
441 let start = clipped_suggestion_range.start - suggestion_range.start;
442 let end = clipped_suggestion_range.end - suggestion_range.start;
443 Some(suggestion.text.chunks_in_range(start..end))
444 } else {
445 None
446 };
447
448 let suffix_chunks = if range.end.0 > suggestion_range.end {
449 let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
450 let end = range.end.0 - suggestion_range.len();
451 Some(self.fold_snapshot.chunks(
452 FoldOffset(start)..FoldOffset(end),
453 language_aware,
454 text_highlights,
455 ))
456 } else {
457 None
458 };
459
460 SuggestionChunks {
461 prefix_chunks,
462 suggestion_chunks,
463 suffix_chunks,
464 highlight_style: suggestion_highlight,
465 }
466 } else {
467 SuggestionChunks {
468 prefix_chunks: Some(self.fold_snapshot.chunks(
469 FoldOffset(range.start.0)..FoldOffset(range.end.0),
470 language_aware,
471 text_highlights,
472 )),
473 suggestion_chunks: None,
474 suffix_chunks: None,
475 highlight_style: None,
476 }
477 }
478 }
479
480 pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
481 let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
482 let start = suggestion.position.to_point(&self.fold_snapshot).0;
483 let end = start + suggestion.text.max_point();
484 start.row..end.row
485 } else {
486 u32::MAX..u32::MAX
487 };
488
489 let fold_buffer_rows = if row <= suggestion_range.start {
490 self.fold_snapshot.buffer_rows(row)
491 } else if row > suggestion_range.end {
492 self.fold_snapshot
493 .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
494 } else {
495 let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
496 rows.next();
497 rows
498 };
499
500 SuggestionBufferRows {
501 current_row: row,
502 suggestion_row_start: suggestion_range.start,
503 suggestion_row_end: suggestion_range.end,
504 fold_buffer_rows,
505 }
506 }
507
508 #[cfg(test)]
509 pub fn text(&self) -> String {
510 self.chunks(Default::default()..self.len(), false, None, None)
511 .map(|chunk| chunk.text)
512 .collect()
513 }
514}
515
516pub struct SuggestionChunks<'a> {
517 prefix_chunks: Option<FoldChunks<'a>>,
518 suggestion_chunks: Option<text::Chunks<'a>>,
519 suffix_chunks: Option<FoldChunks<'a>>,
520 highlight_style: Option<HighlightStyle>,
521}
522
523impl<'a> Iterator for SuggestionChunks<'a> {
524 type Item = Chunk<'a>;
525
526 fn next(&mut self) -> Option<Self::Item> {
527 if let Some(chunks) = self.prefix_chunks.as_mut() {
528 if let Some(chunk) = chunks.next() {
529 return Some(chunk);
530 } else {
531 self.prefix_chunks = None;
532 }
533 }
534
535 if let Some(chunks) = self.suggestion_chunks.as_mut() {
536 if let Some(chunk) = chunks.next() {
537 return Some(Chunk {
538 text: chunk,
539 highlight_style: self.highlight_style,
540 ..Default::default()
541 });
542 } else {
543 self.suggestion_chunks = None;
544 }
545 }
546
547 if let Some(chunks) = self.suffix_chunks.as_mut() {
548 if let Some(chunk) = chunks.next() {
549 return Some(chunk);
550 } else {
551 self.suffix_chunks = None;
552 }
553 }
554
555 None
556 }
557}
558
559#[derive(Clone)]
560pub struct SuggestionBufferRows<'a> {
561 current_row: u32,
562 suggestion_row_start: u32,
563 suggestion_row_end: u32,
564 fold_buffer_rows: FoldBufferRows<'a>,
565}
566
567impl<'a> Iterator for SuggestionBufferRows<'a> {
568 type Item = Option<u32>;
569
570 fn next(&mut self) -> Option<Self::Item> {
571 let row = post_inc(&mut self.current_row);
572 if row <= self.suggestion_row_start || row > self.suggestion_row_end {
573 self.fold_buffer_rows.next()
574 } else {
575 Some(None)
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::{display_map::fold_map::FoldMap, MultiBuffer};
584 use gpui::AppContext;
585 use rand::{prelude::StdRng, Rng};
586 use settings::SettingsStore;
587 use std::{
588 env,
589 ops::{Bound, RangeBounds},
590 };
591
592 #[gpui::test]
593 fn test_basic(cx: &mut AppContext) {
594 let buffer = MultiBuffer::build_simple("abcdefghi", cx);
595 let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
596 let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
597 let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
598 assert_eq!(suggestion_snapshot.text(), "abcdefghi");
599
600 let (suggestion_snapshot, _, _) = suggestion_map.replace(
601 Some(Suggestion {
602 position: 3,
603 text: "123\n456".into(),
604 }),
605 fold_snapshot,
606 Default::default(),
607 );
608 assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
609
610 buffer.update(cx, |buffer, cx| {
611 buffer.edit(
612 [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
613 None,
614 cx,
615 )
616 });
617 let (fold_snapshot, fold_edits) = fold_map.read(
618 buffer.read(cx).snapshot(cx),
619 buffer_edits.consume().into_inner(),
620 );
621 let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
622 assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
623
624 let (mut fold_map_writer, _, _) =
625 fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
626 let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
627 let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
628 assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
629
630 let (mut fold_map_writer, _, _) =
631 fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
632 let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
633 let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
634 assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
635 }
636
637 #[gpui::test(iterations = 100)]
638 fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
639 init_test(cx);
640
641 let operations = env::var("OPERATIONS")
642 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
643 .unwrap_or(10);
644
645 let len = rng.gen_range(0..30);
646 let buffer = if rng.gen() {
647 let text = util::RandomCharIter::new(&mut rng)
648 .take(len)
649 .collect::<String>();
650 MultiBuffer::build_simple(&text, cx)
651 } else {
652 MultiBuffer::build_random(&mut rng, cx)
653 };
654 let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
655 log::info!("buffer text: {:?}", buffer_snapshot.text());
656
657 let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
658 let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
659
660 for _ in 0..operations {
661 let mut suggestion_edits = Patch::default();
662
663 let mut prev_suggestion_text = suggestion_snapshot.text();
664 let mut buffer_edits = Vec::new();
665 match rng.gen_range(0..=100) {
666 0..=29 => {
667 let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
668 suggestion_edits = suggestion_edits.compose(edits);
669 }
670 30..=59 => {
671 for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
672 fold_snapshot = new_fold_snapshot;
673 let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
674 suggestion_edits = suggestion_edits.compose(edits);
675 }
676 }
677 _ => buffer.update(cx, |buffer, cx| {
678 let subscription = buffer.subscribe();
679 let edit_count = rng.gen_range(1..=5);
680 buffer.randomly_mutate(&mut rng, edit_count, cx);
681 buffer_snapshot = buffer.snapshot(cx);
682 let edits = subscription.consume().into_inner();
683 log::info!("editing {:?}", edits);
684 buffer_edits.extend(edits);
685 }),
686 };
687
688 let (new_fold_snapshot, fold_edits) =
689 fold_map.read(buffer_snapshot.clone(), buffer_edits);
690 fold_snapshot = new_fold_snapshot;
691 let (new_suggestion_snapshot, edits) =
692 suggestion_map.sync(fold_snapshot.clone(), fold_edits);
693 suggestion_snapshot = new_suggestion_snapshot;
694 suggestion_edits = suggestion_edits.compose(edits);
695
696 log::info!("buffer text: {:?}", buffer_snapshot.text());
697 log::info!("folds text: {:?}", fold_snapshot.text());
698 log::info!("suggestions text: {:?}", suggestion_snapshot.text());
699
700 let mut expected_text = Rope::from(fold_snapshot.text().as_str());
701 let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
702 if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
703 expected_text.replace(
704 suggestion.position.0..suggestion.position.0,
705 &suggestion.text.to_string(),
706 );
707 let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
708 let suggestion_end = suggestion_start + suggestion.text.max_point();
709 expected_buffer_rows.splice(
710 (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
711 (0..suggestion_end.row - suggestion_start.row).map(|_| None),
712 );
713 }
714 assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
715 for row_start in 0..expected_buffer_rows.len() {
716 assert_eq!(
717 suggestion_snapshot
718 .buffer_rows(row_start as u32)
719 .collect::<Vec<_>>(),
720 &expected_buffer_rows[row_start..],
721 "incorrect buffer rows starting at {}",
722 row_start
723 );
724 }
725
726 for _ in 0..5 {
727 let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
728 end = expected_text.clip_offset(end, Bias::Right);
729 let mut start = rng.gen_range(0..=end);
730 start = expected_text.clip_offset(start, Bias::Right);
731
732 let actual_text = suggestion_snapshot
733 .chunks(
734 SuggestionOffset(start)..SuggestionOffset(end),
735 false,
736 None,
737 None,
738 )
739 .map(|chunk| chunk.text)
740 .collect::<String>();
741 assert_eq!(
742 actual_text,
743 expected_text.slice(start..end).to_string(),
744 "incorrect text in range {:?}",
745 start..end
746 );
747
748 let start_point = SuggestionPoint(expected_text.offset_to_point(start));
749 let end_point = SuggestionPoint(expected_text.offset_to_point(end));
750 assert_eq!(
751 suggestion_snapshot.text_summary_for_range(start_point..end_point),
752 expected_text.slice(start..end).summary()
753 );
754 }
755
756 for edit in suggestion_edits.into_inner() {
757 prev_suggestion_text.replace_range(
758 edit.new.start.0..edit.new.start.0 + edit.old_len().0,
759 &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
760 );
761 }
762 assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
763
764 assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
765 assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
766
767 let mut suggestion_point = SuggestionPoint::default();
768 let mut suggestion_offset = SuggestionOffset::default();
769 for ch in expected_text.chars() {
770 assert_eq!(
771 suggestion_snapshot.to_offset(suggestion_point),
772 suggestion_offset,
773 "invalid to_offset({:?})",
774 suggestion_point
775 );
776 assert_eq!(
777 suggestion_snapshot.to_point(suggestion_offset),
778 suggestion_point,
779 "invalid to_point({:?})",
780 suggestion_offset
781 );
782 assert_eq!(
783 suggestion_snapshot
784 .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
785 suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
786 );
787
788 let mut bytes = [0; 4];
789 for byte in ch.encode_utf8(&mut bytes).as_bytes() {
790 suggestion_offset.0 += 1;
791 if *byte == b'\n' {
792 suggestion_point.0 += Point::new(1, 0);
793 } else {
794 suggestion_point.0 += Point::new(0, 1);
795 }
796
797 let clipped_left_point =
798 suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
799 let clipped_right_point =
800 suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
801 assert!(
802 clipped_left_point <= clipped_right_point,
803 "clipped left point {:?} is greater than clipped right point {:?}",
804 clipped_left_point,
805 clipped_right_point
806 );
807 assert_eq!(
808 clipped_left_point.0,
809 expected_text.clip_point(clipped_left_point.0, Bias::Left)
810 );
811 assert_eq!(
812 clipped_right_point.0,
813 expected_text.clip_point(clipped_right_point.0, Bias::Right)
814 );
815 assert!(clipped_left_point <= suggestion_snapshot.max_point());
816 assert!(clipped_right_point <= suggestion_snapshot.max_point());
817
818 if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
819 let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
820 let suggestion_end = suggestion_start + suggestion.text.max_point();
821 let invalid_range = (
822 Bound::Excluded(suggestion_start),
823 Bound::Included(suggestion_end),
824 );
825 assert!(
826 !invalid_range.contains(&clipped_left_point.0),
827 "clipped left point {:?} is inside invalid suggestion range {:?}",
828 clipped_left_point,
829 invalid_range
830 );
831 assert!(
832 !invalid_range.contains(&clipped_right_point.0),
833 "clipped right point {:?} is inside invalid suggestion range {:?}",
834 clipped_right_point,
835 invalid_range
836 );
837 }
838 }
839 }
840 }
841 }
842
843 fn init_test(cx: &mut AppContext) {
844 cx.set_global(SettingsStore::test(cx));
845 theme::init((), cx);
846 }
847
848 impl SuggestionMap {
849 pub fn randomly_mutate(
850 &self,
851 rng: &mut impl Rng,
852 ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
853 let fold_snapshot = self.0.lock().fold_snapshot.clone();
854 let new_suggestion = if rng.gen_bool(0.3) {
855 None
856 } else {
857 let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
858 let len = rng.gen_range(0..30);
859 Some(Suggestion {
860 position: index,
861 text: util::RandomCharIter::new(rng)
862 .take(len)
863 .filter(|ch| *ch != '\r')
864 .collect::<String>()
865 .as_str()
866 .into(),
867 })
868 };
869
870 log::info!("replacing suggestion with {:?}", new_suggestion);
871 let (snapshot, edits, _) =
872 self.replace(new_suggestion, fold_snapshot, Default::default());
873 (snapshot, edits)
874 }
875 }
876}