1use super::{
2 Highlights,
3 dimensions::RowDelta,
4 fold_map::{Chunk, FoldRows},
5 tab_map::{self, TabEdit, TabPoint, TabSnapshot},
6};
7use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Task};
8use language::Point;
9use multi_buffer::{MultiBufferSnapshot, RowInfo};
10use smol::future::yield_now;
11use std::{cmp, collections::VecDeque, mem, ops::Range, sync::LazyLock, time::Duration};
12use sum_tree::{Bias, Cursor, Dimensions, SumTree};
13use text::Patch;
14
15pub use super::tab_map::TextSummary;
16pub type WrapEdit = text::Edit<WrapRow>;
17pub type WrapPatch = text::Patch<WrapRow>;
18
19#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
20pub struct WrapRow(pub u32);
21
22const WRAP_YIELD_ROW_INTERVAL: usize = 100;
23
24impl_for_row_types! {
25 WrapRow => RowDelta
26}
27
28/// Handles soft wrapping of text.
29///
30/// See the [`display_map` module documentation](crate::display_map) for more information.
31pub struct WrapMap {
32 snapshot: WrapSnapshot,
33 pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
34 interpolated_edits: WrapPatch,
35 edits_since_sync: WrapPatch,
36 wrap_width: Option<Pixels>,
37 background_task: Option<Task<()>>,
38 font_with_size: (Font, Pixels),
39}
40
41#[derive(Clone)]
42pub struct WrapSnapshot {
43 pub(super) tab_snapshot: TabSnapshot,
44 transforms: SumTree<Transform>,
45 interpolated: bool,
46}
47
48impl std::ops::Deref for WrapSnapshot {
49 type Target = TabSnapshot;
50
51 fn deref(&self) -> &Self::Target {
52 &self.tab_snapshot
53 }
54}
55
56#[derive(Clone, Debug, Default, Eq, PartialEq)]
57struct Transform {
58 summary: TransformSummary,
59 display_text: Option<&'static str>,
60}
61
62#[derive(Clone, Debug, Default, Eq, PartialEq)]
63struct TransformSummary {
64 input: TextSummary,
65 output: TextSummary,
66}
67
68#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
69pub struct WrapPoint(pub Point);
70
71pub struct WrapChunks<'a> {
72 input_chunks: tab_map::TabChunks<'a>,
73 input_chunk: Chunk<'a>,
74 output_position: WrapPoint,
75 max_output_row: WrapRow,
76 transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
77 snapshot: &'a WrapSnapshot,
78}
79
80#[derive(Clone)]
81pub struct WrapRows<'a> {
82 input_buffer_rows: FoldRows<'a>,
83 input_buffer_row: RowInfo,
84 output_row: WrapRow,
85 soft_wrapped: bool,
86 max_output_row: WrapRow,
87 transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
88}
89
90impl WrapRows<'_> {
91 #[ztracing::instrument(skip_all)]
92 pub(crate) fn seek(&mut self, start_row: WrapRow) {
93 self.transforms
94 .seek(&WrapPoint::new(start_row, 0), Bias::Left);
95 let mut input_row = self.transforms.start().1.row();
96 if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
97 input_row += (start_row - self.transforms.start().0.row()).0;
98 }
99 self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic());
100 self.input_buffer_rows.seek(input_row);
101 self.input_buffer_row = self.input_buffer_rows.next().unwrap();
102 self.output_row = start_row;
103 }
104}
105
106impl WrapMap {
107 #[ztracing::instrument(skip_all)]
108 pub fn new(
109 tab_snapshot: TabSnapshot,
110 font: Font,
111 font_size: Pixels,
112 wrap_width: Option<Pixels>,
113 cx: &mut App,
114 ) -> (Entity<Self>, WrapSnapshot) {
115 let handle = cx.new(|cx| {
116 let mut this = Self {
117 font_with_size: (font, font_size),
118 wrap_width: None,
119 pending_edits: Default::default(),
120 interpolated_edits: Default::default(),
121 edits_since_sync: Default::default(),
122 snapshot: WrapSnapshot::new(tab_snapshot),
123 background_task: None,
124 };
125 this.set_wrap_width(wrap_width, cx);
126 mem::take(&mut this.edits_since_sync);
127 this
128 });
129 let snapshot = handle.read(cx).snapshot.clone();
130 (handle, snapshot)
131 }
132
133 #[cfg(test)]
134 pub fn is_rewrapping(&self) -> bool {
135 self.background_task.is_some()
136 }
137
138 #[ztracing::instrument(skip_all)]
139 pub fn sync(
140 &mut self,
141 tab_snapshot: TabSnapshot,
142 edits: Vec<TabEdit>,
143 cx: &mut Context<Self>,
144 ) -> (WrapSnapshot, WrapPatch) {
145 if self.wrap_width.is_some() {
146 self.pending_edits.push_back((tab_snapshot, edits));
147 self.flush_edits(cx);
148 } else {
149 self.edits_since_sync = self
150 .edits_since_sync
151 .compose(self.snapshot.interpolate(tab_snapshot, &edits));
152 self.snapshot.interpolated = false;
153 }
154
155 (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
156 }
157
158 #[ztracing::instrument(skip_all)]
159 pub fn set_font_with_size(
160 &mut self,
161 font: Font,
162 font_size: Pixels,
163 cx: &mut Context<Self>,
164 ) -> bool {
165 let font_with_size = (font, font_size);
166
167 if font_with_size == self.font_with_size {
168 false
169 } else {
170 self.font_with_size = font_with_size;
171 self.rewrap(cx);
172 true
173 }
174 }
175
176 #[ztracing::instrument(skip_all)]
177 pub fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut Context<Self>) -> bool {
178 if wrap_width == self.wrap_width {
179 return false;
180 }
181
182 self.wrap_width = wrap_width;
183 self.rewrap(cx);
184 true
185 }
186
187 #[ztracing::instrument(skip_all)]
188 fn rewrap(&mut self, cx: &mut Context<Self>) {
189 self.background_task.take();
190 self.interpolated_edits.clear();
191 self.pending_edits.clear();
192
193 if let Some(wrap_width) = self.wrap_width {
194 let mut new_snapshot = self.snapshot.clone();
195
196 let text_system = cx.text_system();
197 let (font, font_size) = self.font_with_size.clone();
198 let mut line_wrapper = text_system.line_wrapper(font, font_size);
199 let tab_snapshot = new_snapshot.tab_snapshot.clone();
200 let total_rows = tab_snapshot.max_point().row() as usize + 1;
201 let range = TabPoint::zero()..tab_snapshot.max_point();
202 let tab_edits = [TabEdit {
203 old: range.clone(),
204 new: range,
205 }];
206
207 if total_rows < WRAP_YIELD_ROW_INTERVAL {
208 let edits = smol::block_on(new_snapshot.update(
209 tab_snapshot,
210 &tab_edits,
211 wrap_width,
212 &mut line_wrapper,
213 ));
214 self.snapshot = new_snapshot;
215 self.edits_since_sync = self.edits_since_sync.compose(&edits);
216 } else {
217 let task = cx.background_spawn(async move {
218 let edits = new_snapshot
219 .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
220 .await;
221 (new_snapshot, edits)
222 });
223
224 match cx
225 .foreground_executor()
226 .block_with_timeout(Duration::from_millis(5), task)
227 {
228 Ok((snapshot, edits)) => {
229 self.snapshot = snapshot;
230 self.edits_since_sync = self.edits_since_sync.compose(&edits);
231 }
232 Err(wrap_task) => {
233 self.background_task = Some(cx.spawn(async move |this, cx| {
234 let (snapshot, edits) = wrap_task.await;
235 this.update(cx, |this, cx| {
236 this.snapshot = snapshot;
237 this.edits_since_sync = this
238 .edits_since_sync
239 .compose(mem::take(&mut this.interpolated_edits).invert())
240 .compose(&edits);
241 this.background_task = None;
242 this.flush_edits(cx);
243 cx.notify();
244 })
245 .ok();
246 }));
247 }
248 }
249 }
250 } else {
251 let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
252 self.snapshot.transforms = SumTree::default();
253 let summary = self.snapshot.tab_snapshot.text_summary();
254 if !summary.lines.is_zero() {
255 self.snapshot
256 .transforms
257 .push(Transform::isomorphic(summary), ());
258 }
259 let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
260 self.snapshot.interpolated = false;
261 self.edits_since_sync = self.edits_since_sync.compose(Patch::new(vec![WrapEdit {
262 old: WrapRow(0)..WrapRow(old_rows),
263 new: WrapRow(0)..WrapRow(new_rows),
264 }]));
265 }
266 }
267
268 #[ztracing::instrument(skip_all)]
269 fn flush_edits(&mut self, cx: &mut Context<Self>) {
270 if !self.snapshot.interpolated {
271 let mut to_remove_len = 0;
272 for (tab_snapshot, _) in &self.pending_edits {
273 if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
274 to_remove_len += 1;
275 } else {
276 break;
277 }
278 }
279 self.pending_edits.drain(..to_remove_len);
280 }
281
282 if self.pending_edits.is_empty() {
283 return;
284 }
285
286 if let Some(wrap_width) = self.wrap_width
287 && self.background_task.is_none()
288 {
289 let mut pending_edits = self.pending_edits.clone();
290 let mut snapshot = self.snapshot.clone();
291 let text_system = cx.text_system().clone();
292 let (font, font_size) = self.font_with_size.clone();
293 let mut line_wrapper = text_system.line_wrapper(font, font_size);
294
295 if pending_edits.len() == 1
296 && let Some((_, tab_edits)) = pending_edits.back()
297 && let [edit] = &**tab_edits
298 && ((edit.new.end.row().saturating_sub(edit.new.start.row()) + 1) as usize)
299 < WRAP_YIELD_ROW_INTERVAL
300 && let Some((tab_snapshot, tab_edits)) = pending_edits.pop_back()
301 {
302 let wrap_edits = smol::block_on(snapshot.update(
303 tab_snapshot,
304 &tab_edits,
305 wrap_width,
306 &mut line_wrapper,
307 ));
308 self.snapshot = snapshot;
309 self.edits_since_sync = self.edits_since_sync.compose(&wrap_edits);
310 } else {
311 let update_task = cx.background_spawn(async move {
312 let mut edits = Patch::default();
313 for (tab_snapshot, tab_edits) in pending_edits {
314 let wrap_edits = snapshot
315 .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
316 .await;
317 edits = edits.compose(&wrap_edits);
318 }
319 (snapshot, edits)
320 });
321
322 match cx
323 .foreground_executor()
324 .block_with_timeout(Duration::from_millis(1), update_task)
325 {
326 Ok((snapshot, output_edits)) => {
327 self.snapshot = snapshot;
328 self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
329 }
330 Err(update_task) => {
331 self.background_task = Some(cx.spawn(async move |this, cx| {
332 let (snapshot, edits) = update_task.await;
333 this.update(cx, |this, cx| {
334 this.snapshot = snapshot;
335 this.edits_since_sync = this
336 .edits_since_sync
337 .compose(mem::take(&mut this.interpolated_edits).invert())
338 .compose(&edits);
339 this.background_task = None;
340 this.flush_edits(cx);
341 cx.notify();
342 })
343 .ok();
344 }));
345 }
346 }
347 }
348 }
349
350 let was_interpolated = self.snapshot.interpolated;
351 let mut to_remove_len = 0;
352 for (tab_snapshot, edits) in &self.pending_edits {
353 if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
354 to_remove_len += 1;
355 } else {
356 let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
357 self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
358 self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
359 }
360 }
361
362 if !was_interpolated {
363 self.pending_edits.drain(..to_remove_len);
364 }
365 }
366}
367
368impl WrapSnapshot {
369 #[ztracing::instrument(skip_all)]
370 fn new(tab_snapshot: TabSnapshot) -> Self {
371 let mut transforms = SumTree::default();
372 let extent = tab_snapshot.text_summary();
373 if !extent.lines.is_zero() {
374 transforms.push(Transform::isomorphic(extent), ());
375 }
376 Self {
377 transforms,
378 tab_snapshot,
379 interpolated: true,
380 }
381 }
382
383 #[ztracing::instrument(skip_all)]
384 pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
385 self.tab_snapshot.buffer_snapshot()
386 }
387
388 #[ztracing::instrument(skip_all)]
389 fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> WrapPatch {
390 let mut new_transforms;
391 if tab_edits.is_empty() {
392 new_transforms = self.transforms.clone();
393 } else {
394 let mut old_cursor = self.transforms.cursor::<TabPoint>(());
395
396 let mut tab_edits_iter = tab_edits.iter().peekable();
397 new_transforms =
398 old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right);
399
400 while let Some(edit) = tab_edits_iter.next() {
401 if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
402 let summary = new_tab_snapshot.text_summary_for_range(
403 TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
404 );
405 new_transforms.push_or_extend(Transform::isomorphic(summary));
406 }
407
408 if !edit.new.is_empty() {
409 new_transforms.push_or_extend(Transform::isomorphic(
410 new_tab_snapshot.text_summary_for_range(edit.new.clone()),
411 ));
412 }
413
414 old_cursor.seek_forward(&edit.old.end, Bias::Right);
415 if let Some(next_edit) = tab_edits_iter.peek() {
416 if next_edit.old.start > old_cursor.end() {
417 if old_cursor.end() > edit.old.end {
418 let summary = self
419 .tab_snapshot
420 .text_summary_for_range(edit.old.end..old_cursor.end());
421 new_transforms.push_or_extend(Transform::isomorphic(summary));
422 }
423
424 old_cursor.next();
425 new_transforms
426 .append(old_cursor.slice(&next_edit.old.start, Bias::Right), ());
427 }
428 } else {
429 if old_cursor.end() > edit.old.end {
430 let summary = self
431 .tab_snapshot
432 .text_summary_for_range(edit.old.end..old_cursor.end());
433 new_transforms.push_or_extend(Transform::isomorphic(summary));
434 }
435 old_cursor.next();
436 new_transforms.append(old_cursor.suffix(), ());
437 }
438 }
439 }
440
441 let old_snapshot = mem::replace(
442 self,
443 WrapSnapshot {
444 tab_snapshot: new_tab_snapshot,
445 transforms: new_transforms,
446 interpolated: true,
447 },
448 );
449 self.check_invariants();
450 old_snapshot.compute_edits(tab_edits, self)
451 }
452
453 #[ztracing::instrument(skip_all)]
454 async fn update(
455 &mut self,
456 new_tab_snapshot: TabSnapshot,
457 tab_edits: &[TabEdit],
458 wrap_width: Pixels,
459 line_wrapper: &mut LineWrapper,
460 ) -> WrapPatch {
461 #[derive(Debug)]
462 struct RowEdit {
463 old_rows: Range<u32>,
464 new_rows: Range<u32>,
465 }
466
467 let mut tab_edits_iter = tab_edits.iter().peekable();
468 let mut row_edits = Vec::with_capacity(tab_edits.len());
469 while let Some(edit) = tab_edits_iter.next() {
470 let mut row_edit = RowEdit {
471 old_rows: edit.old.start.row()..edit.old.end.row() + 1,
472 new_rows: edit.new.start.row()..edit.new.end.row() + 1,
473 };
474
475 while let Some(next_edit) = tab_edits_iter.peek() {
476 if next_edit.old.start.row() <= row_edit.old_rows.end {
477 row_edit.old_rows.end =
478 cmp::max(row_edit.old_rows.end, next_edit.old.end.row() + 1);
479 row_edit.new_rows.end =
480 cmp::max(row_edit.new_rows.end, next_edit.new.end.row() + 1);
481 tab_edits_iter.next();
482 } else {
483 break;
484 }
485 }
486
487 row_edits.push(row_edit);
488 }
489
490 let mut new_transforms;
491 if row_edits.is_empty() {
492 new_transforms = self.transforms.clone();
493 } else {
494 let mut row_edits = row_edits.into_iter().peekable();
495 let mut old_cursor = self.transforms.cursor::<TabPoint>(());
496
497 new_transforms = old_cursor.slice(
498 &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
499 Bias::Right,
500 );
501
502 while let Some(edit) = row_edits.next() {
503 if edit.new_rows.start > new_transforms.summary().input.lines.row {
504 let summary = new_tab_snapshot.text_summary_for_range(
505 TabPoint(new_transforms.summary().input.lines)
506 ..TabPoint::new(edit.new_rows.start, 0),
507 );
508 new_transforms.push_or_extend(Transform::isomorphic(summary));
509 }
510
511 let mut line = String::new();
512 let mut line_fragments = Vec::new();
513 let mut remaining = None;
514 let mut chunks = new_tab_snapshot.chunks(
515 TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
516 false,
517 Highlights::default(),
518 );
519 let mut edit_transforms = Vec::<Transform>::new();
520 for (i, _) in (edit.new_rows.start..edit.new_rows.end).enumerate() {
521 while let Some(chunk) = remaining.take().or_else(|| chunks.next()) {
522 if let Some(ix) = chunk.text.find('\n') {
523 let (prefix, suffix) = chunk.text.split_at(ix + 1);
524 line_fragments.push(gpui::LineFragment::text(prefix));
525 line.push_str(prefix);
526 remaining = Some(Chunk {
527 text: suffix,
528 ..chunk
529 });
530 break;
531 } else {
532 if let Some(width) =
533 chunk.renderer.as_ref().and_then(|r| r.measured_width)
534 {
535 line_fragments
536 .push(gpui::LineFragment::element(width, chunk.text.len()));
537 } else {
538 line_fragments.push(gpui::LineFragment::text(chunk.text));
539 }
540 line.push_str(chunk.text);
541 }
542 }
543
544 if line.is_empty() {
545 break;
546 }
547
548 let mut prev_boundary_ix = 0;
549 for boundary in line_wrapper.wrap_line(&line_fragments, wrap_width) {
550 let wrapped = &line[prev_boundary_ix..boundary.ix];
551 push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped));
552 edit_transforms.push(Transform::wrap(boundary.next_indent));
553 prev_boundary_ix = boundary.ix;
554 }
555
556 if prev_boundary_ix < line.len() {
557 push_isomorphic(
558 &mut edit_transforms,
559 TextSummary::from(&line[prev_boundary_ix..]),
560 );
561 }
562
563 line.clear();
564 line_fragments.clear();
565 if i % WRAP_YIELD_ROW_INTERVAL == WRAP_YIELD_ROW_INTERVAL - 1 {
566 yield_now().await;
567 }
568 }
569
570 let mut edit_transforms = edit_transforms.into_iter();
571 if let Some(transform) = edit_transforms.next() {
572 new_transforms.push_or_extend(transform);
573 }
574 new_transforms.extend(edit_transforms, ());
575
576 old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right);
577 if let Some(next_edit) = row_edits.peek() {
578 if next_edit.old_rows.start > old_cursor.end().row() {
579 if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) {
580 let summary = self.tab_snapshot.text_summary_for_range(
581 TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(),
582 );
583 new_transforms.push_or_extend(Transform::isomorphic(summary));
584 }
585 old_cursor.next();
586 new_transforms.append(
587 old_cursor
588 .slice(&TabPoint::new(next_edit.old_rows.start, 0), Bias::Right),
589 (),
590 );
591 }
592 yield_now().await;
593 } else {
594 if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) {
595 let summary = self.tab_snapshot.text_summary_for_range(
596 TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(),
597 );
598 new_transforms.push_or_extend(Transform::isomorphic(summary));
599 }
600 old_cursor.next();
601 new_transforms.append(old_cursor.suffix(), ());
602 }
603 }
604 }
605
606 let old_snapshot = mem::replace(
607 self,
608 WrapSnapshot {
609 tab_snapshot: new_tab_snapshot,
610 transforms: new_transforms,
611 interpolated: false,
612 },
613 );
614 self.check_invariants();
615 old_snapshot.compute_edits(tab_edits, self)
616 }
617
618 #[ztracing::instrument(skip_all)]
619 fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> WrapPatch {
620 let mut wrap_edits = Vec::with_capacity(tab_edits.len());
621 let mut old_cursor = self.transforms.cursor::<TransformSummary>(());
622 let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>(());
623 for mut tab_edit in tab_edits.iter().cloned() {
624 tab_edit.old.start.0.column = 0;
625 tab_edit.old.end.0 += Point::new(1, 0);
626 tab_edit.new.start.0.column = 0;
627 tab_edit.new.end.0 += Point::new(1, 0);
628
629 old_cursor.seek(&tab_edit.old.start, Bias::Right);
630 let mut old_start = old_cursor.start().output.lines;
631 old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
632
633 old_cursor.seek_forward(&tab_edit.old.end, Bias::Right);
634 let mut old_end = old_cursor.start().output.lines;
635 old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
636
637 new_cursor.seek(&tab_edit.new.start, Bias::Right);
638 let mut new_start = new_cursor.start().output.lines;
639 new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
640
641 new_cursor.seek_forward(&tab_edit.new.end, Bias::Right);
642 let mut new_end = new_cursor.start().output.lines;
643 new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
644
645 wrap_edits.push(WrapEdit {
646 old: WrapRow(old_start.row)..WrapRow(old_end.row),
647 new: WrapRow(new_start.row)..WrapRow(new_end.row),
648 });
649 }
650
651 wrap_edits = consolidate_wrap_edits(wrap_edits);
652 Patch::new(wrap_edits)
653 }
654
655 #[ztracing::instrument(skip_all)]
656 pub(crate) fn chunks<'a>(
657 &'a self,
658 rows: Range<WrapRow>,
659 language_aware: bool,
660 highlights: Highlights<'a>,
661 ) -> WrapChunks<'a> {
662 let output_start = WrapPoint::new(rows.start, 0);
663 let output_end = WrapPoint::new(rows.end, 0);
664 let mut transforms = self
665 .transforms
666 .cursor::<Dimensions<WrapPoint, TabPoint>>(());
667 transforms.seek(&output_start, Bias::Right);
668 let mut input_start = TabPoint(transforms.start().1.0);
669 if transforms.item().is_some_and(|t| t.is_isomorphic()) {
670 input_start.0 += output_start.0 - transforms.start().0.0;
671 }
672 let input_end = self.to_tab_point(output_end);
673 let max_point = self.tab_snapshot.max_point();
674 let input_start = input_start.min(max_point);
675 let input_end = input_end.min(max_point);
676 WrapChunks {
677 input_chunks: self.tab_snapshot.chunks(
678 input_start..input_end,
679 language_aware,
680 highlights,
681 ),
682 input_chunk: Default::default(),
683 output_position: output_start,
684 max_output_row: rows.end,
685 transforms,
686 snapshot: self,
687 }
688 }
689
690 #[ztracing::instrument(skip_all)]
691 pub fn max_point(&self) -> WrapPoint {
692 WrapPoint(self.transforms.summary().output.lines)
693 }
694
695 #[ztracing::instrument(skip_all)]
696 pub fn line_len(&self, row: WrapRow) -> u32 {
697 let (start, _, item) = self.transforms.find::<Dimensions<WrapPoint, TabPoint>, _>(
698 (),
699 &WrapPoint::new(row + WrapRow(1), 0),
700 Bias::Left,
701 );
702 if item.is_some_and(|transform| transform.is_isomorphic()) {
703 let overshoot = row - start.0.row();
704 let tab_row = start.1.row() + overshoot.0;
705 let tab_line_len = self.tab_snapshot.line_len(tab_row);
706 if overshoot.0 == 0 {
707 start.0.column() + (tab_line_len - start.1.column())
708 } else {
709 tab_line_len
710 }
711 } else {
712 start.0.column()
713 }
714 }
715
716 #[ztracing::instrument(skip_all, fields(rows))]
717 pub fn text_summary_for_range(&self, rows: Range<WrapRow>) -> TextSummary {
718 let mut summary = TextSummary::default();
719
720 let start = WrapPoint::new(rows.start, 0);
721 let end = WrapPoint::new(rows.end, 0);
722
723 let mut cursor = self
724 .transforms
725 .cursor::<Dimensions<WrapPoint, TabPoint>>(());
726 cursor.seek(&start, Bias::Right);
727 if let Some(transform) = cursor.item() {
728 let start_in_transform = start.0 - cursor.start().0.0;
729 let end_in_transform = cmp::min(end, cursor.end().0).0 - cursor.start().0.0;
730 if transform.is_isomorphic() {
731 let tab_start = TabPoint(cursor.start().1.0 + start_in_transform);
732 let tab_end = TabPoint(cursor.start().1.0 + end_in_transform);
733 summary += &self.tab_snapshot.text_summary_for_range(tab_start..tab_end);
734 } else {
735 debug_assert_eq!(start_in_transform.row, end_in_transform.row);
736 let indent_len = end_in_transform.column - start_in_transform.column;
737 summary += &TextSummary {
738 lines: Point::new(0, indent_len),
739 first_line_chars: indent_len,
740 last_line_chars: indent_len,
741 longest_row: 0,
742 longest_row_chars: indent_len,
743 };
744 }
745
746 cursor.next();
747 }
748
749 if rows.end > cursor.start().0.row() {
750 summary += &cursor
751 .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right)
752 .output;
753
754 if let Some(transform) = cursor.item() {
755 let end_in_transform = end.0 - cursor.start().0.0;
756 if transform.is_isomorphic() {
757 let char_start = cursor.start().1;
758 let char_end = TabPoint(char_start.0 + end_in_transform);
759 summary += &self
760 .tab_snapshot
761 .text_summary_for_range(char_start..char_end);
762 } else {
763 debug_assert_eq!(end_in_transform, Point::new(1, 0));
764 summary += &TextSummary {
765 lines: Point::new(1, 0),
766 first_line_chars: 0,
767 last_line_chars: 0,
768 longest_row: 0,
769 longest_row_chars: 0,
770 };
771 }
772 }
773 }
774
775 summary
776 }
777
778 #[ztracing::instrument(skip_all)]
779 pub fn soft_wrap_indent(&self, row: WrapRow) -> Option<u32> {
780 let (.., item) = self.transforms.find::<WrapPoint, _>(
781 (),
782 &WrapPoint::new(row + WrapRow(1), 0),
783 Bias::Right,
784 );
785 item.and_then(|transform| {
786 if transform.is_isomorphic() {
787 None
788 } else {
789 Some(transform.summary.output.lines.column)
790 }
791 })
792 }
793
794 #[ztracing::instrument(skip_all)]
795 pub fn longest_row(&self) -> u32 {
796 self.transforms.summary().output.longest_row
797 }
798
799 #[ztracing::instrument(skip_all)]
800 pub fn row_infos(&self, start_row: WrapRow) -> WrapRows<'_> {
801 let mut transforms = self
802 .transforms
803 .cursor::<Dimensions<WrapPoint, TabPoint>>(());
804 transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
805 let mut input_row = transforms.start().1.row();
806 if transforms.item().is_some_and(|t| t.is_isomorphic()) {
807 input_row += (start_row - transforms.start().0.row()).0;
808 }
809 let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic());
810 let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
811 let input_buffer_row = input_buffer_rows.next().unwrap();
812 WrapRows {
813 transforms,
814 input_buffer_row,
815 input_buffer_rows,
816 output_row: start_row,
817 soft_wrapped,
818 max_output_row: self.max_point().row(),
819 }
820 }
821
822 #[ztracing::instrument(skip_all)]
823 pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
824 let (start, _, item) =
825 self.transforms
826 .find::<Dimensions<WrapPoint, TabPoint>, _>((), &point, Bias::Right);
827 let mut tab_point = start.1.0;
828 if item.is_some_and(|t| t.is_isomorphic()) {
829 tab_point += point.0 - start.0.0;
830 }
831 TabPoint(tab_point)
832 }
833
834 #[ztracing::instrument(skip_all)]
835 pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
836 self.tab_snapshot
837 .tab_point_to_point(self.to_tab_point(point), bias)
838 }
839
840 #[ztracing::instrument(skip_all)]
841 pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
842 self.tab_point_to_wrap_point(self.tab_snapshot.point_to_tab_point(point, bias))
843 }
844
845 #[ztracing::instrument(skip_all)]
846 pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
847 let (start, ..) =
848 self.transforms
849 .find::<Dimensions<TabPoint, WrapPoint>, _>((), &point, Bias::Right);
850 WrapPoint(start.1.0 + (point.0 - start.0.0))
851 }
852
853 #[ztracing::instrument(skip_all)]
854 pub fn wrap_point_cursor(&self) -> WrapPointCursor<'_> {
855 WrapPointCursor {
856 cursor: self
857 .transforms
858 .cursor::<Dimensions<TabPoint, WrapPoint>>(()),
859 }
860 }
861
862 #[ztracing::instrument(skip_all)]
863 pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
864 if bias == Bias::Left {
865 let (start, _, item) = self
866 .transforms
867 .find::<WrapPoint, _>((), &point, Bias::Right);
868 if item.is_some_and(|t| !t.is_isomorphic()) {
869 point = start;
870 *point.column_mut() -= 1;
871 }
872 }
873
874 self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
875 }
876
877 /// Try to find a TabRow start that is also a WrapRow start
878 /// Every TabRow start is a WrapRow start
879 #[ztracing::instrument(skip_all, fields(point=?point))]
880 pub fn prev_row_boundary(&self, point: WrapPoint) -> WrapRow {
881 if self.transforms.is_empty() {
882 return WrapRow(0);
883 }
884
885 let point = WrapPoint::new(point.row(), 0);
886
887 let mut cursor = self
888 .transforms
889 .cursor::<Dimensions<WrapPoint, TabPoint>>(());
890
891 cursor.seek(&point, Bias::Right);
892 if cursor.item().is_none() {
893 cursor.prev();
894 }
895
896 // real newline fake fake
897 // text: helloworldasldlfjasd\njdlasfalsk\naskdjfasdkfj\n
898 // dimensions v v v v v
899 // transforms |-------|-----NW----|-----W------|-----W------|
900 // cursor ^ ^^^^^^^^^^^^^ ^
901 // (^) ^^^^^^^^^^^^^^
902 // point: ^
903 // point(col_zero): (^)
904
905 while let Some(transform) = cursor.item() {
906 if transform.is_isomorphic() {
907 // this transform only has real linefeeds
908 let tab_summary = &transform.summary.input;
909 // is the wrap just before the end of the transform a tab row?
910 // thats only if this transform has at least one newline
911 //
912 // "this wrap row is a tab row" <=> self.to_tab_point(WrapPoint::new(wrap_row, 0)).column() == 0
913
914 // Note on comparison:
915 // We have code that relies on this to be row > 1
916 // It should work with row >= 1 but it does not :(
917 //
918 // That means that if every line is wrapped we walk back all the
919 // way to the start. Which invalidates the entire state triggering
920 // a full re-render.
921 if tab_summary.lines.row > 1 {
922 let wrap_point_at_end = cursor.end().0.row();
923 return cmp::min(wrap_point_at_end - RowDelta(1), point.row());
924 } else if cursor.start().1.column() == 0 {
925 return cmp::min(cursor.end().0.row(), point.row());
926 }
927 }
928
929 cursor.prev();
930 }
931
932 WrapRow(0)
933 }
934
935 #[ztracing::instrument(skip_all)]
936 pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<WrapRow> {
937 point.0 += Point::new(1, 0);
938
939 let mut cursor = self
940 .transforms
941 .cursor::<Dimensions<WrapPoint, TabPoint>>(());
942 cursor.seek(&point, Bias::Right);
943 while let Some(transform) = cursor.item() {
944 if transform.is_isomorphic() && cursor.start().1.column() == 0 {
945 return Some(cmp::max(cursor.start().0.row(), point.row()));
946 } else {
947 cursor.next();
948 }
949 }
950
951 None
952 }
953
954 #[cfg(test)]
955 pub fn text(&self) -> String {
956 self.text_chunks(WrapRow(0)).collect()
957 }
958
959 #[cfg(test)]
960 pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
961 self.chunks(
962 wrap_row..self.max_point().row() + WrapRow(1),
963 false,
964 Highlights::default(),
965 )
966 .map(|h| h.text)
967 }
968
969 #[ztracing::instrument(skip_all)]
970 fn check_invariants(&self) {
971 #[cfg(test)]
972 {
973 assert_eq!(
974 TabPoint::from(self.transforms.summary().input.lines),
975 self.tab_snapshot.max_point()
976 );
977
978 {
979 let mut transforms = self.transforms.cursor::<()>(()).peekable();
980 while let Some(transform) = transforms.next() {
981 if let Some(next_transform) = transforms.peek() {
982 assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
983 }
984 }
985 }
986
987 let text = language::Rope::from(self.text().as_str());
988 let mut input_buffer_rows = self.tab_snapshot.rows(0);
989 let mut expected_buffer_rows = Vec::new();
990 let mut prev_tab_row = 0;
991 for display_row in 0..=self.max_point().row().0 {
992 let display_row = WrapRow(display_row);
993 let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
994 if tab_point.row() == prev_tab_row && display_row != WrapRow(0) {
995 expected_buffer_rows.push(None);
996 } else {
997 expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
998 }
999
1000 prev_tab_row = tab_point.row();
1001 assert_eq!(self.line_len(display_row), text.line_len(display_row.0));
1002 }
1003
1004 for start_display_row in 0..expected_buffer_rows.len() {
1005 assert_eq!(
1006 self.row_infos(WrapRow(start_display_row as u32))
1007 .map(|row_info| row_info.buffer_row)
1008 .collect::<Vec<_>>(),
1009 &expected_buffer_rows[start_display_row..],
1010 "invalid buffer_rows({}..)",
1011 start_display_row
1012 );
1013 }
1014 }
1015 }
1016}
1017
1018pub struct WrapPointCursor<'transforms> {
1019 cursor: Cursor<'transforms, 'static, Transform, Dimensions<TabPoint, WrapPoint>>,
1020}
1021
1022impl WrapPointCursor<'_> {
1023 #[ztracing::instrument(skip_all)]
1024 pub fn map(&mut self, point: TabPoint) -> WrapPoint {
1025 let cursor = &mut self.cursor;
1026 if cursor.did_seek() {
1027 cursor.seek_forward(&point, Bias::Right);
1028 } else {
1029 cursor.seek(&point, Bias::Right);
1030 }
1031 WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0))
1032 }
1033}
1034
1035impl WrapChunks<'_> {
1036 #[ztracing::instrument(skip_all)]
1037 pub(crate) fn seek(&mut self, rows: Range<WrapRow>) {
1038 let output_start = WrapPoint::new(rows.start, 0);
1039 let output_end = WrapPoint::new(rows.end, 0);
1040 self.transforms.seek(&output_start, Bias::Right);
1041 let mut input_start = TabPoint(self.transforms.start().1.0);
1042 if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
1043 input_start.0 += output_start.0 - self.transforms.start().0.0;
1044 }
1045 let input_end = self.snapshot.to_tab_point(output_end);
1046 let max_point = self.snapshot.tab_snapshot.max_point();
1047 let input_start = input_start.min(max_point);
1048 let input_end = input_end.min(max_point);
1049 self.input_chunks.seek(input_start..input_end);
1050 self.input_chunk = Chunk::default();
1051 self.output_position = output_start;
1052 self.max_output_row = rows.end;
1053 }
1054}
1055
1056impl<'a> Iterator for WrapChunks<'a> {
1057 type Item = Chunk<'a>;
1058
1059 #[ztracing::instrument(skip_all)]
1060 fn next(&mut self) -> Option<Self::Item> {
1061 if self.output_position.row() >= self.max_output_row {
1062 return None;
1063 }
1064
1065 let transform = self.transforms.item()?;
1066 if let Some(display_text) = transform.display_text {
1067 let mut start_ix = 0;
1068 let mut end_ix = display_text.len();
1069 let mut summary = transform.summary.output.lines;
1070
1071 if self.output_position > self.transforms.start().0 {
1072 // Exclude newline starting prior to the desired row.
1073 start_ix = 1;
1074 summary.row = 0;
1075 } else if self.output_position.row() + WrapRow(1) >= self.max_output_row {
1076 // Exclude soft indentation ending after the desired row.
1077 end_ix = 1;
1078 summary.column = 0;
1079 }
1080
1081 self.output_position.0 += summary;
1082 self.transforms.next();
1083 return Some(Chunk {
1084 text: &display_text[start_ix..end_ix],
1085 ..Default::default()
1086 });
1087 }
1088
1089 if self.input_chunk.text.is_empty() {
1090 self.input_chunk = self.input_chunks.next()?;
1091 }
1092
1093 let mut input_len = 0;
1094 let transform_end = self.transforms.end().0;
1095 for c in self.input_chunk.text.chars() {
1096 let char_len = c.len_utf8();
1097 input_len += char_len;
1098 if c == '\n' {
1099 *self.output_position.row_mut() += 1;
1100 *self.output_position.column_mut() = 0;
1101 } else {
1102 *self.output_position.column_mut() += char_len as u32;
1103 }
1104
1105 if self.output_position >= transform_end {
1106 self.transforms.next();
1107 break;
1108 }
1109 }
1110
1111 let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
1112
1113 let mask = 1u128.unbounded_shl(input_len as u32).wrapping_sub(1);
1114 let chars = self.input_chunk.chars & mask;
1115 let tabs = self.input_chunk.tabs & mask;
1116 let newlines = self.input_chunk.newlines & mask;
1117 self.input_chunk.tabs = self.input_chunk.tabs.unbounded_shr(input_len as u32);
1118 self.input_chunk.chars = self.input_chunk.chars.unbounded_shr(input_len as u32);
1119 self.input_chunk.newlines = self.input_chunk.newlines.unbounded_shr(input_len as u32);
1120
1121 self.input_chunk.text = suffix;
1122 Some(Chunk {
1123 text: prefix,
1124 chars,
1125 tabs,
1126 newlines,
1127 ..self.input_chunk.clone()
1128 })
1129 }
1130}
1131
1132impl Iterator for WrapRows<'_> {
1133 type Item = RowInfo;
1134
1135 #[ztracing::instrument(skip_all)]
1136 fn next(&mut self) -> Option<Self::Item> {
1137 if self.output_row > self.max_output_row {
1138 return None;
1139 }
1140
1141 let buffer_row = self.input_buffer_row;
1142 let soft_wrapped = self.soft_wrapped;
1143 let diff_status = self.input_buffer_row.diff_status;
1144
1145 self.output_row += WrapRow(1);
1146 self.transforms
1147 .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
1148 if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
1149 self.input_buffer_row = self.input_buffer_rows.next().unwrap();
1150 self.soft_wrapped = false;
1151 } else {
1152 self.soft_wrapped = true;
1153 }
1154
1155 Some(if soft_wrapped {
1156 RowInfo {
1157 buffer_id: None,
1158 buffer_row: None,
1159 multibuffer_row: None,
1160 diff_status,
1161 expand_info: None,
1162 wrapped_buffer_row: buffer_row.buffer_row,
1163 }
1164 } else {
1165 buffer_row
1166 })
1167 }
1168}
1169
1170impl Transform {
1171 #[ztracing::instrument(skip_all)]
1172 fn isomorphic(summary: TextSummary) -> Self {
1173 #[cfg(test)]
1174 assert!(!summary.lines.is_zero());
1175
1176 Self {
1177 summary: TransformSummary {
1178 input: summary.clone(),
1179 output: summary,
1180 },
1181 display_text: None,
1182 }
1183 }
1184
1185 #[ztracing::instrument(skip_all)]
1186 fn wrap(indent: u32) -> Self {
1187 static WRAP_TEXT: LazyLock<String> = LazyLock::new(|| {
1188 let mut wrap_text = String::new();
1189 wrap_text.push('\n');
1190 wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
1191 wrap_text
1192 });
1193
1194 Self {
1195 summary: TransformSummary {
1196 input: TextSummary::default(),
1197 output: TextSummary {
1198 lines: Point::new(1, indent),
1199 first_line_chars: 0,
1200 last_line_chars: indent,
1201 longest_row: 1,
1202 longest_row_chars: indent,
1203 },
1204 },
1205 display_text: Some(&WRAP_TEXT[..1 + indent as usize]),
1206 }
1207 }
1208
1209 fn is_isomorphic(&self) -> bool {
1210 self.display_text.is_none()
1211 }
1212}
1213
1214impl sum_tree::Item for Transform {
1215 type Summary = TransformSummary;
1216
1217 fn summary(&self, _cx: ()) -> Self::Summary {
1218 self.summary.clone()
1219 }
1220}
1221
1222fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
1223 if let Some(last_transform) = transforms.last_mut()
1224 && last_transform.is_isomorphic()
1225 {
1226 last_transform.summary.input += &summary;
1227 last_transform.summary.output += &summary;
1228 return;
1229 }
1230 transforms.push(Transform::isomorphic(summary));
1231}
1232
1233trait SumTreeExt {
1234 fn push_or_extend(&mut self, transform: Transform);
1235}
1236
1237impl SumTreeExt for SumTree<Transform> {
1238 #[ztracing::instrument(skip_all)]
1239 fn push_or_extend(&mut self, transform: Transform) {
1240 let mut transform = Some(transform);
1241 self.update_last(
1242 |last_transform| {
1243 if last_transform.is_isomorphic() && transform.as_ref().unwrap().is_isomorphic() {
1244 let transform = transform.take().unwrap();
1245 last_transform.summary.input += &transform.summary.input;
1246 last_transform.summary.output += &transform.summary.output;
1247 }
1248 },
1249 (),
1250 );
1251
1252 if let Some(transform) = transform {
1253 self.push(transform, ());
1254 }
1255 }
1256}
1257
1258impl WrapPoint {
1259 pub fn new(row: WrapRow, column: u32) -> Self {
1260 Self(Point::new(row.0, column))
1261 }
1262
1263 pub fn row(self) -> WrapRow {
1264 WrapRow(self.0.row)
1265 }
1266
1267 pub fn row_mut(&mut self) -> &mut u32 {
1268 &mut self.0.row
1269 }
1270
1271 pub fn column(self) -> u32 {
1272 self.0.column
1273 }
1274
1275 pub fn column_mut(&mut self) -> &mut u32 {
1276 &mut self.0.column
1277 }
1278}
1279
1280impl sum_tree::ContextLessSummary for TransformSummary {
1281 fn zero() -> Self {
1282 Default::default()
1283 }
1284
1285 fn add_summary(&mut self, other: &Self) {
1286 self.input += &other.input;
1287 self.output += &other.output;
1288 }
1289}
1290
1291impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
1292 fn zero(_cx: ()) -> Self {
1293 Default::default()
1294 }
1295
1296 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
1297 self.0 += summary.input.lines;
1298 }
1299}
1300
1301impl sum_tree::SeekTarget<'_, TransformSummary, TransformSummary> for TabPoint {
1302 #[ztracing::instrument(skip_all)]
1303 fn cmp(&self, cursor_location: &TransformSummary, _: ()) -> std::cmp::Ordering {
1304 Ord::cmp(&self.0, &cursor_location.input.lines)
1305 }
1306}
1307
1308impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
1309 fn zero(_cx: ()) -> Self {
1310 Default::default()
1311 }
1312
1313 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
1314 self.0 += summary.output.lines;
1315 }
1316}
1317
1318fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
1319 let _old_alloc_ptr = edits.as_ptr();
1320 let mut wrap_edits = edits.into_iter();
1321
1322 if let Some(mut first_edit) = wrap_edits.next() {
1323 // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
1324 #[allow(clippy::filter_map_identity)]
1325 let mut v: Vec<_> = wrap_edits
1326 .scan(&mut first_edit, |prev_edit, edit| {
1327 if prev_edit.old.end >= edit.old.start {
1328 prev_edit.old.end = edit.old.end;
1329 prev_edit.new.end = edit.new.end;
1330 Some(None) // Skip this edit, it's merged
1331 } else {
1332 let prev = std::mem::replace(*prev_edit, edit);
1333 Some(Some(prev)) // Yield the previous edit
1334 }
1335 })
1336 .filter_map(|x| x)
1337 .collect();
1338 v.push(first_edit.clone());
1339 debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Wrap edits were reallocated");
1340 v
1341 } else {
1342 vec![]
1343 }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348 use super::*;
1349 use crate::{
1350 MultiBuffer,
1351 display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
1352 test::test_font,
1353 };
1354 use gpui::{LineFragment, px, test::observe};
1355 use rand::prelude::*;
1356 use settings::SettingsStore;
1357 use smol::stream::StreamExt;
1358 use std::{cmp, env, num::NonZeroU32};
1359 use text::Rope;
1360 use theme::LoadThemes;
1361
1362 #[gpui::test]
1363 async fn test_prev_row_boundary(cx: &mut gpui::TestAppContext) {
1364 init_test(cx);
1365
1366 fn test_wrap_snapshot(
1367 text: &str,
1368 soft_wrap_every: usize, // font size multiple
1369 cx: &mut gpui::TestAppContext,
1370 ) -> WrapSnapshot {
1371 let text_system = cx.read(|cx| cx.text_system().clone());
1372 let tab_size = 4.try_into().unwrap();
1373 let font = test_font();
1374 let _font_id = text_system.resolve_font(&font);
1375 let font_size = px(14.0);
1376 // this is very much an estimate to try and get the wrapping to
1377 // occur at `soft_wrap_every` we check that it pans out for every test case
1378 let soft_wrapping = Some(font_size * soft_wrap_every * 0.6);
1379
1380 let buffer = cx.new(|cx| language::Buffer::local(text, cx));
1381 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
1382 let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
1383 let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1384 let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
1385 let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
1386 let tabs_snapshot = tab_map.set_max_expansion_column(32);
1387 let (_wrap_map, wrap_snapshot) =
1388 cx.update(|cx| WrapMap::new(tabs_snapshot, font, font_size, soft_wrapping, cx));
1389
1390 wrap_snapshot
1391 }
1392
1393 // These two should pass but dont, see the comparison note in
1394 // prev_row_boundary about why.
1395 //
1396 // // 0123 4567 wrap_rows
1397 // let wrap_snapshot = test_wrap_snapshot("1234\n5678", 1, cx);
1398 // assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8");
1399 // let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
1400 // assert_eq!(row.0, 3);
1401
1402 // // 012 345 678 wrap_rows
1403 // let wrap_snapshot = test_wrap_snapshot("123\n456\n789", 1, cx);
1404 // assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
1405 // let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
1406 // assert_eq!(row.0, 5);
1407
1408 // 012345678 wrap_rows
1409 let wrap_snapshot = test_wrap_snapshot("123456789", 1, cx);
1410 assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
1411 let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
1412 assert_eq!(row.0, 0);
1413
1414 // 111 2222 44 wrap_rows
1415 let wrap_snapshot = test_wrap_snapshot("123\n4567\n\n89", 4, cx);
1416 assert_eq!(wrap_snapshot.text(), "123\n4567\n\n89");
1417 let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
1418 assert_eq!(row.0, 2);
1419
1420 // 11 2223 wrap_rows
1421 let wrap_snapshot = test_wrap_snapshot("12\n3456\n\n", 3, cx);
1422 assert_eq!(wrap_snapshot.text(), "12\n345\n6\n\n");
1423 let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
1424 assert_eq!(row.0, 3);
1425 }
1426
1427 #[gpui::test(iterations = 100)]
1428 async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
1429 // todo this test is flaky
1430 init_test(cx);
1431
1432 cx.background_executor.set_block_on_ticks(0..=50);
1433 let operations = env::var("OPERATIONS")
1434 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1435 .unwrap_or(10);
1436
1437 let text_system = cx.read(|cx| cx.text_system().clone());
1438 let mut wrap_width = if rng.random_bool(0.1) {
1439 None
1440 } else {
1441 Some(px(rng.random_range(0.0..=1000.0)))
1442 };
1443 let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
1444
1445 let font = test_font();
1446 let _font_id = text_system.resolve_font(&font);
1447 let font_size = px(14.0);
1448
1449 log::info!("Tab size: {}", tab_size);
1450 log::info!("Wrap width: {:?}", wrap_width);
1451
1452 let buffer = cx.update(|cx| {
1453 if rng.random() {
1454 MultiBuffer::build_random(&mut rng, cx)
1455 } else {
1456 let len = rng.random_range(0..10);
1457 let text = util::RandomCharIter::new(&mut rng)
1458 .take(len)
1459 .collect::<String>();
1460 MultiBuffer::build_simple(&text, cx)
1461 }
1462 });
1463 let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
1464 log::info!("Buffer text: {:?}", buffer_snapshot.text());
1465 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1466 log::info!("InlayMap text: {:?}", inlay_snapshot.text());
1467 let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
1468 log::info!("FoldMap text: {:?}", fold_snapshot.text());
1469 let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
1470 let tabs_snapshot = tab_map.set_max_expansion_column(32);
1471 log::info!("TabMap text: {:?}", tabs_snapshot.text());
1472
1473 let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
1474 let expected_text = wrap_text(&tabs_snapshot, wrap_width, &mut line_wrapper);
1475
1476 let (wrap_map, _) =
1477 cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx));
1478 let mut notifications = observe(&wrap_map, cx);
1479
1480 if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
1481 notifications.next().await.unwrap();
1482 }
1483
1484 let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
1485 assert!(!map.is_rewrapping());
1486 map.sync(tabs_snapshot.clone(), Vec::new(), cx)
1487 });
1488
1489 let actual_text = initial_snapshot.text();
1490 assert_eq!(
1491 actual_text,
1492 expected_text,
1493 "unwrapped text is: {:?}",
1494 tabs_snapshot.text()
1495 );
1496 log::info!("Wrapped text: {:?}", actual_text);
1497
1498 let mut next_inlay_id = 0;
1499 let mut edits = Vec::new();
1500 for _i in 0..operations {
1501 log::info!("{} ==============================================", _i);
1502
1503 let mut buffer_edits = Vec::new();
1504 match rng.random_range(0..=100) {
1505 0..=19 => {
1506 wrap_width = if rng.random_bool(0.2) {
1507 None
1508 } else {
1509 Some(px(rng.random_range(0.0..=1000.0)))
1510 };
1511 log::info!("Setting wrap width to {:?}", wrap_width);
1512 wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
1513 }
1514 20..=39 => {
1515 for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
1516 let (tabs_snapshot, tab_edits) =
1517 tab_map.sync(fold_snapshot, fold_edits, tab_size);
1518 let (mut snapshot, wrap_edits) =
1519 wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
1520 snapshot.check_invariants();
1521 snapshot.verify_chunks(&mut rng);
1522 edits.push((snapshot, wrap_edits));
1523 }
1524 }
1525 40..=59 => {
1526 let (inlay_snapshot, inlay_edits) =
1527 inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1528 let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
1529 let (tabs_snapshot, tab_edits) =
1530 tab_map.sync(fold_snapshot, fold_edits, tab_size);
1531 let (mut snapshot, wrap_edits) =
1532 wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
1533 snapshot.check_invariants();
1534 snapshot.verify_chunks(&mut rng);
1535 edits.push((snapshot, wrap_edits));
1536 }
1537 _ => {
1538 buffer.update(cx, |buffer, cx| {
1539 let subscription = buffer.subscribe();
1540 let edit_count = rng.random_range(1..=5);
1541 buffer.randomly_mutate(&mut rng, edit_count, cx);
1542 buffer_snapshot = buffer.snapshot(cx);
1543 buffer_edits.extend(subscription.consume());
1544 });
1545 }
1546 }
1547
1548 log::info!("Buffer text: {:?}", buffer_snapshot.text());
1549 let (inlay_snapshot, inlay_edits) =
1550 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
1551 log::info!("InlayMap text: {:?}", inlay_snapshot.text());
1552 let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
1553 log::info!("FoldMap text: {:?}", fold_snapshot.text());
1554 let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
1555 log::info!("TabMap text: {:?}", tabs_snapshot.text());
1556
1557 let expected_text = wrap_text(&tabs_snapshot, wrap_width, &mut line_wrapper);
1558 let (mut snapshot, wrap_edits) =
1559 wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
1560 snapshot.check_invariants();
1561 snapshot.verify_chunks(&mut rng);
1562 edits.push((snapshot, wrap_edits));
1563
1564 if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.random_bool(0.4) {
1565 log::info!("Waiting for wrapping to finish");
1566 while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
1567 notifications.next().await.unwrap();
1568 }
1569 wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
1570 }
1571
1572 if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
1573 let (mut wrapped_snapshot, wrap_edits) = wrap_map.update(cx, |map, cx| {
1574 map.sync(tabs_snapshot.clone(), Vec::new(), cx)
1575 });
1576 let actual_text = wrapped_snapshot.text();
1577 let actual_longest_row = wrapped_snapshot.longest_row();
1578 log::info!("Wrapping finished: {:?}", actual_text);
1579 wrapped_snapshot.check_invariants();
1580 wrapped_snapshot.verify_chunks(&mut rng);
1581 edits.push((wrapped_snapshot.clone(), wrap_edits));
1582 assert_eq!(
1583 actual_text,
1584 expected_text,
1585 "unwrapped text is: {:?}",
1586 tabs_snapshot.text()
1587 );
1588
1589 let mut summary = TextSummary::default();
1590 for (ix, item) in wrapped_snapshot
1591 .transforms
1592 .items(())
1593 .into_iter()
1594 .enumerate()
1595 {
1596 summary += &item.summary.output;
1597 log::info!("{} summary: {:?}", ix, item.summary.output,);
1598 }
1599
1600 if tab_size.get() == 1
1601 || !wrapped_snapshot
1602 .tab_snapshot
1603 .fold_snapshot
1604 .text()
1605 .contains('\t')
1606 {
1607 let mut expected_longest_rows = Vec::new();
1608 let mut longest_line_len = -1;
1609 for (row, line) in expected_text.split('\n').enumerate() {
1610 let line_char_count = line.chars().count() as isize;
1611 if line_char_count > longest_line_len {
1612 expected_longest_rows.clear();
1613 longest_line_len = line_char_count;
1614 }
1615 if line_char_count >= longest_line_len {
1616 expected_longest_rows.push(row as u32);
1617 }
1618 }
1619
1620 assert!(
1621 expected_longest_rows.contains(&actual_longest_row),
1622 "incorrect longest row {}. expected {:?} with length {}",
1623 actual_longest_row,
1624 expected_longest_rows,
1625 longest_line_len,
1626 )
1627 }
1628 }
1629 }
1630
1631 let mut initial_text = Rope::from(initial_snapshot.text().as_str());
1632 for (snapshot, patch) in edits {
1633 let snapshot_text = Rope::from(snapshot.text().as_str());
1634 for edit in &patch {
1635 let old_start = initial_text.point_to_offset(Point::new(edit.new.start.0, 0));
1636 let old_end = initial_text.point_to_offset(cmp::min(
1637 Point::new(edit.new.start.0 + (edit.old.end - edit.old.start).0, 0),
1638 initial_text.max_point(),
1639 ));
1640 let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start.0, 0));
1641 let new_end = snapshot_text.point_to_offset(cmp::min(
1642 Point::new(edit.new.end.0, 0),
1643 snapshot_text.max_point(),
1644 ));
1645 let new_text = snapshot_text
1646 .chunks_in_range(new_start..new_end)
1647 .collect::<String>();
1648
1649 initial_text.replace(old_start..old_end, &new_text);
1650 }
1651 assert_eq!(initial_text.to_string(), snapshot_text.to_string());
1652 }
1653
1654 if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
1655 log::info!("Waiting for wrapping to finish");
1656 while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
1657 notifications.next().await.unwrap();
1658 }
1659 }
1660 wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
1661 }
1662
1663 fn init_test(cx: &mut gpui::TestAppContext) {
1664 cx.update(|cx| {
1665 let settings = SettingsStore::test(cx);
1666 cx.set_global(settings);
1667 theme::init(LoadThemes::JustBase, cx);
1668 });
1669 }
1670
1671 fn wrap_text(
1672 tab_snapshot: &TabSnapshot,
1673 wrap_width: Option<Pixels>,
1674 line_wrapper: &mut LineWrapper,
1675 ) -> String {
1676 if let Some(wrap_width) = wrap_width {
1677 let mut wrapped_text = String::new();
1678 for (row, line) in tab_snapshot.text().split('\n').enumerate() {
1679 if row > 0 {
1680 wrapped_text.push('\n');
1681 }
1682
1683 let mut prev_ix = 0;
1684 for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
1685 wrapped_text.push_str(&line[prev_ix..boundary.ix]);
1686 wrapped_text.push('\n');
1687 wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
1688 prev_ix = boundary.ix;
1689 }
1690 wrapped_text.push_str(&line[prev_ix..]);
1691 }
1692
1693 wrapped_text
1694 } else {
1695 tab_snapshot.text()
1696 }
1697 }
1698
1699 impl WrapSnapshot {
1700 fn verify_chunks(&mut self, rng: &mut impl Rng) {
1701 for _ in 0..5 {
1702 let mut end_row = rng.random_range(0..=self.max_point().row().0);
1703 let start_row = rng.random_range(0..=end_row);
1704 end_row += 1;
1705
1706 let mut expected_text = self.text_chunks(WrapRow(start_row)).collect::<String>();
1707 if expected_text.ends_with('\n') {
1708 expected_text.push('\n');
1709 }
1710 let mut expected_text = expected_text
1711 .lines()
1712 .take((end_row - start_row) as usize)
1713 .collect::<Vec<_>>()
1714 .join("\n");
1715 if end_row <= self.max_point().row().0 {
1716 expected_text.push('\n');
1717 }
1718
1719 let actual_text = self
1720 .chunks(
1721 WrapRow(start_row)..WrapRow(end_row),
1722 true,
1723 Highlights::default(),
1724 )
1725 .map(|c| c.text)
1726 .collect::<String>();
1727 assert_eq!(
1728 expected_text,
1729 actual_text,
1730 "chunks != highlighted_chunks for rows {:?}",
1731 start_row..end_row
1732 );
1733 }
1734 }
1735 }
1736}