1#[derive(Debug, Clone, PartialEq)]
2pub struct VisualLine {
3 pub logical_row: usize,
4 pub start_col: usize,
6 pub end_col: usize,
8 pub start_byte: usize,
10 pub end_byte: usize,
12 pub is_first_visual_line: bool,
13}
14
15impl VisualLine {
16 pub fn content<'a>(&self, source: &'a str) -> &'a str {
19 &source[self.start_byte..self.end_byte]
20 }
21}
22
23struct Cluster {
27 char_pos: usize,
29 byte_pos: usize,
31 width: usize,
33 is_ws: bool,
36}
37
38fn wrap_one_row(
60 logical_row: usize,
61 line: &str,
62 width: usize,
63 inset: usize,
64 rendered_row: &[bool],
65 scratch: &mut Vec<Cluster>,
66 out: &mut Vec<VisualLine>,
67) {
68 use unicode_segmentation::UnicodeSegmentation;
69
70 let width = if inset == 0 {
75 width
76 } else {
77 width.saturating_sub(inset).max(1)
78 };
79
80 scratch.clear();
81 let mut char_pos = 0usize;
82 for (byte_pos, g) in line.grapheme_indices(true) {
83 let char_len = g.chars().count();
84 let is_ws = char_len == 1 && g.chars().next().is_some_and(char::is_whitespace);
85 scratch.push(Cluster {
86 char_pos,
87 byte_pos,
88 width: super::markdown::cluster_display_width(g),
89 is_ws,
90 });
91 char_pos += char_len;
92 }
93 let total_chars = char_pos;
94 let cl: &[Cluster] = scratch.as_slice();
95 if cl.is_empty() || width == 0 {
96 out.push(VisualLine {
97 logical_row,
98 start_col: 0,
99 end_col: 0,
100 start_byte: 0,
101 end_byte: 0,
102 is_first_visual_line: true,
103 });
104 return;
105 }
106
107 let is_rendered = |char_pos: usize| -> bool {
108 if char_pos < rendered_row.len() {
109 rendered_row[char_pos]
110 } else {
111 true
112 }
113 };
114 let vis_width = |idx: usize| -> usize {
116 if is_rendered(cl[idx].char_pos) {
117 cl[idx].width
118 } else {
119 0
120 }
121 };
122 let char_at = |idx: usize| -> usize {
124 if idx < cl.len() {
125 cl[idx].char_pos
126 } else {
127 total_chars
128 }
129 };
130 let byte_at = |idx: usize| -> usize {
131 if idx < cl.len() {
132 cl[idx].byte_pos
133 } else {
134 line.len()
135 }
136 };
137
138 let total = cl.len(); let mut start = 0; let mut is_first = true;
141
142 while start < total {
143 let fit_end = {
146 let mut rcount = 0usize;
147 let mut pos = start;
148 while pos < total {
149 let r = vis_width(pos);
150 if rcount + r > width {
151 break;
152 }
153 rcount += r;
154 pos += 1;
155 }
156 if pos == start { start + 1 } else { pos }
160 };
161
162 if fit_end >= total {
163 out.push(VisualLine {
164 logical_row,
165 start_col: char_at(start),
166 end_col: total_chars,
167 start_byte: byte_at(start),
168 end_byte: line.len(),
169 is_first_visual_line: is_first,
170 });
171 break;
172 }
173
174 let (content_end, next_start) = if cl[fit_end].is_ws {
176 (fit_end, fit_end + 1)
177 } else {
178 match cl[start..fit_end]
179 .iter()
180 .enumerate()
181 .rev()
182 .find(|(_, c)| c.is_ws)
183 {
184 Some((i, _)) => (start + i, start + i + 1),
185 None => (fit_end, fit_end), }
187 };
188
189 out.push(VisualLine {
190 logical_row,
191 start_col: char_at(start),
192 end_col: char_at(content_end),
193 start_byte: byte_at(start),
194 end_byte: byte_at(content_end),
195 is_first_visual_line: is_first,
196 });
197 start = next_start;
198 is_first = false;
199 }
200}
201
202#[derive(Clone)]
203pub struct WordWrapLayout {
204 visual_lines: Vec<VisualLine>,
205 row_starts: Vec<usize>,
208}
209
210impl WordWrapLayout {
211 pub fn compute(lines: &[String], width: u16, rendered: &[Vec<bool>], insets: &[usize]) -> Self {
215 let width = width as usize;
216 let mut visual_lines = Vec::new();
217 let mut row_starts = Vec::with_capacity(lines.len());
218
219 if lines.is_empty() {
220 return Self::default();
221 }
222
223 let mut scratch: Vec<Cluster> = Vec::new();
226 for (row, line) in lines.iter().enumerate() {
227 row_starts.push(visual_lines.len());
228 let rendered_row = rendered.get(row).map(|v| v.as_slice()).unwrap_or(&[]);
229 let inset = insets.get(row).copied().unwrap_or(0);
230 wrap_one_row(
231 row,
232 line,
233 width,
234 inset,
235 rendered_row,
236 &mut scratch,
237 &mut visual_lines,
238 );
239 }
240
241 Self {
242 visual_lines,
243 row_starts,
244 }
245 }
246
247 pub fn splice_range(
258 &mut self,
259 lines: &[String],
260 width: u16,
261 rendered: &[Vec<bool>],
262 insets: &[usize],
263 row_range: std::ops::Range<usize>,
264 ) {
265 if row_range.is_empty() {
266 return;
267 }
268 let width = width as usize;
269 debug_assert!(
270 row_range.end <= lines.len(),
271 "splice_range: row_range.end {} > lines.len() {}",
272 row_range.end,
273 lines.len(),
274 );
275 debug_assert!(
276 row_range.start <= self.row_starts.len(),
277 "splice_range: row_range.start {} > row_starts.len() {}",
278 row_range.start,
279 self.row_starts.len(),
280 );
281
282 let old_vstart = self.row_starts[row_range.start];
284 let old_vend = if row_range.end < self.row_starts.len() {
285 self.row_starts[row_range.end]
286 } else {
287 self.visual_lines.len()
288 };
289
290 let mut new_slice: Vec<VisualLine> = Vec::new();
295 let mut new_row_starts_for_range: Vec<usize> = Vec::with_capacity(row_range.len());
296 let mut scratch: Vec<Cluster> = Vec::new();
297 for row in row_range.clone() {
298 new_row_starts_for_range.push(new_slice.len());
299 let rendered_row = rendered.get(row).map(|v| v.as_slice()).unwrap_or(&[]);
300 let inset = insets.get(row).copied().unwrap_or(0);
301 wrap_one_row(
302 row,
303 &lines[row],
304 width,
305 inset,
306 rendered_row,
307 &mut scratch,
308 &mut new_slice,
309 );
310 }
311
312 let new_vcount = new_slice.len();
314 self.visual_lines.splice(old_vstart..old_vend, new_slice);
315
316 let old_vcount = old_vend - old_vstart;
318 let delta_i = new_vcount as isize - old_vcount as isize;
319
320 for (i, local_start) in new_row_starts_for_range.into_iter().enumerate() {
322 self.row_starts[row_range.start + i] = old_vstart + local_start;
323 }
324
325 if delta_i != 0 {
327 for rs in &mut self.row_starts[row_range.end..] {
328 *rs = ((*rs as isize) + delta_i) as usize;
329 }
330 }
331 }
332
333 pub fn total_visual_lines(&self) -> usize {
334 self.visual_lines.len()
335 }
336
337 pub fn row_starts_len(&self) -> usize {
341 self.row_starts.len()
342 }
343
344 pub fn visual_lines(&self) -> &[VisualLine] {
345 &self.visual_lines
346 }
347
348 pub fn logical_to_visual(&self, row: usize, col: usize) -> (usize, usize) {
350 let row = row.min(self.row_starts.len().saturating_sub(1));
351 let first = self.row_starts.get(row).copied().unwrap_or(0);
352 let vrow = self.visual_lines[first..]
353 .iter()
354 .enumerate()
355 .take_while(|(_, vl)| vl.logical_row == row)
356 .filter(|(_, vl)| vl.start_col <= col)
357 .last()
358 .map(|(i, _)| first + i)
359 .unwrap_or(first);
360 let vl = &self.visual_lines[vrow];
361 (vrow, col.saturating_sub(vl.start_col))
362 }
363
364 pub fn visual_to_logical(&self, vrow: usize, vcol: usize) -> (usize, usize) {
366 let vrow = vrow.min(self.visual_lines.len().saturating_sub(1));
367 let vl = &self.visual_lines[vrow];
368 let col = (vl.start_col + vcol).min(vl.end_col);
369 (vl.logical_row, col)
370 }
371}
372
373impl Default for WordWrapLayout {
374 fn default() -> Self {
375 Self {
376 visual_lines: vec![VisualLine {
377 logical_row: 0,
378 start_col: 0,
379 end_col: 0,
380 start_byte: 0,
381 end_byte: 0,
382 is_first_visual_line: true,
383 }],
384 row_starts: vec![0],
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 fn ls(s: &str) -> Vec<String> {
394 s.lines().map(str::to_owned).collect()
395 }
396
397 fn content_of<'a>(vl: &VisualLine, source: &'a str) -> &'a str {
399 vl.content(source)
400 }
401
402 #[test]
403 fn left_inset_reduces_effective_wrap_width() {
404 let lines = vec!["aaaa bbbb".to_string()];
406 let no_inset = WordWrapLayout::compute(&lines, 9, &[], &[0]);
407 assert_eq!(no_inset.total_visual_lines(), 1);
408
409 let inset = WordWrapLayout::compute(&lines, 9, &[], &[2]);
411 assert_eq!(inset.total_visual_lines(), 2);
412 assert_eq!(content_of(&inset.visual_lines()[0], &lines[0]), "aaaa");
413 assert_eq!(content_of(&inset.visual_lines()[1], &lines[0]), "bbbb");
414 }
415
416 #[test]
417 fn empty_input_produces_one_visual_line() {
418 let layout = WordWrapLayout::compute(&[], 40, &[], &[]);
419 assert_eq!(layout.total_visual_lines(), 1);
420 assert_eq!(layout.visual_lines()[0].logical_row, 0);
421 assert!(layout.visual_lines()[0].is_first_visual_line);
422 }
423
424 #[test]
425 fn empty_string_produces_one_visual_line() {
426 let src = String::new();
427 let layout = WordWrapLayout::compute(std::slice::from_ref(&src), 40, &[], &[]);
428 assert_eq!(layout.total_visual_lines(), 1);
429 assert_eq!(content_of(&layout.visual_lines()[0], &src), "");
430 assert!(layout.visual_lines()[0].is_first_visual_line);
431 }
432
433 #[test]
434 fn short_line_fits_on_one_visual_line() {
435 let lines = ls("hello world");
436 let layout = WordWrapLayout::compute(&lines, 40, &[], &[]);
437 assert_eq!(layout.total_visual_lines(), 1);
438 assert_eq!(
439 content_of(&layout.visual_lines()[0], &lines[0]),
440 "hello world"
441 );
442 assert!(layout.visual_lines()[0].is_first_visual_line);
443 }
444
445 #[test]
446 fn long_line_wraps_at_whitespace() {
447 let lines = ls("hello world foo");
449 let layout = WordWrapLayout::compute(&lines, 11, &[], &[]);
450 assert_eq!(layout.total_visual_lines(), 2);
451 assert_eq!(
452 content_of(&layout.visual_lines()[0], &lines[0]),
453 "hello world"
454 );
455 assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "foo");
456 assert!(layout.visual_lines()[0].is_first_visual_line);
457 assert!(!layout.visual_lines()[1].is_first_visual_line);
458 }
459
460 #[test]
461 fn long_word_hard_breaks_at_width() {
462 let lines = vec!["abcdefgh".to_string()];
463 let layout = WordWrapLayout::compute(&lines, 4, &[], &[]);
464 assert_eq!(layout.total_visual_lines(), 2);
465 assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "abcd");
466 assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "efgh");
467 }
468
469 #[test]
470 fn two_logical_lines_have_correct_logical_rows() {
471 let layout = WordWrapLayout::compute(&ls("abc\nxyz"), 10, &[], &[]);
472 assert_eq!(layout.total_visual_lines(), 2);
473 assert_eq!(layout.visual_lines()[0].logical_row, 0);
474 assert_eq!(layout.visual_lines()[1].logical_row, 1);
475 }
476
477 #[test]
478 fn unicode_chars_counted_not_bytes() {
479 let lines = vec!["あいう".to_string()];
483 let layout = WordWrapLayout::compute(&lines, 4, &[], &[]);
484 assert_eq!(layout.total_visual_lines(), 2);
485 assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "あい");
486 assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "う");
487 }
488
489 #[test]
490 fn full_width_glyph_counts_as_two_columns() {
491 let lines = vec!["あい".to_string()];
493 let layout = WordWrapLayout::compute(&lines, 2, &[], &[]);
494 assert_eq!(layout.total_visual_lines(), 2);
495 assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "あ");
496 assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "い");
497 }
498
499 #[test]
500 fn multi_codepoint_cluster_never_split() {
501 let combined = "e\u{0301}fg"; let lines = vec![combined.to_string()];
507 let layout = WordWrapLayout::compute(&lines, 1, &[], &[]);
508 assert_eq!(layout.total_visual_lines(), 3);
511 assert_eq!(content_of(&layout.visual_lines()[0], combined), "e\u{0301}");
512 assert_eq!(content_of(&layout.visual_lines()[1], combined), "f");
513 assert_eq!(content_of(&layout.visual_lines()[2], combined), "g");
514 }
515
516 #[test]
517 fn logical_to_visual_start_of_line() {
518 let layout = WordWrapLayout::compute(&ls("hello world"), 40, &[], &[]);
519 assert_eq!(layout.logical_to_visual(0, 0), (0, 0));
520 }
521
522 #[test]
523 fn logical_to_visual_wrapped_cursor() {
524 let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[], &[]);
525 let (vrow, vcol) = layout.logical_to_visual(0, 12);
526 assert_eq!(vrow, 1);
527 assert_eq!(vcol, 0);
528 }
529
530 #[test]
531 fn visual_to_logical_first_line() {
532 let layout = WordWrapLayout::compute(&ls("hello"), 40, &[], &[]);
533 assert_eq!(layout.visual_to_logical(0, 3), (0, 3));
534 }
535
536 #[test]
537 fn visual_to_logical_accounts_for_start_col() {
538 let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[], &[]);
539 let (row, col) = layout.visual_to_logical(1, 0);
540 assert_eq!(row, 0);
541 assert_eq!(col, 12);
542 }
543
544 #[test]
545 fn row_starts_index_multi_line_multi_wrap() {
546 let lines = vec![
547 "abc".to_string(),
548 "hello world foo".to_string(),
549 "xyz".to_string(),
550 ];
551 let layout = WordWrapLayout::compute(&lines, 11, &[], &[]);
552 assert_eq!(layout.row_starts, vec![0, 1, 3]);
553 assert_eq!(layout.logical_to_visual(2, 0), (3, 0));
554 }
555
556 #[test]
557 fn coordinate_roundtrip_vrow_zero() {
558 let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[], &[]);
559 let (row, col) = layout.visual_to_logical(0, 3);
560 let (vrow2, vcol2) = layout.logical_to_visual(row, col);
561 assert_eq!((vrow2, vcol2), (0, 3));
562 }
563
564 #[test]
565 fn byte_offsets_correct_for_unicode() {
566 let lines = vec!["あいう".to_string()];
569 let layout = WordWrapLayout::compute(&lines, 4, &[], &[]);
570 let vl0 = &layout.visual_lines()[0];
571 let vl1 = &layout.visual_lines()[1];
572 assert_eq!((vl0.start_byte, vl0.end_byte), (0, 6)); assert_eq!((vl1.start_byte, vl1.end_byte), (6, 9)); }
575
576 #[test]
577 fn splice_range_full_buffer_equals_compute() {
578 let lines = ls("hello world\nfoo bar baz\nlast line");
579 let mut layout = WordWrapLayout::compute(&lines, 40, &[], &[]);
580 layout.splice_range(&lines, 40, &[], &[], 0..lines.len());
581 let fresh = WordWrapLayout::compute(&lines, 40, &[], &[]);
582 assert_eq!(layout.visual_lines(), fresh.visual_lines());
583 assert_eq!(layout.row_starts, fresh.row_starts);
584 }
585
586 #[test]
587 fn splice_range_middle_row_only() {
588 let lines_before = ls("alpha beta\nfoo bar\ngamma delta");
590 let layout_before = WordWrapLayout::compute(&lines_before, 40, &[], &[]);
591
592 let lines_after = ls("alpha beta\nFOO BAR\ngamma delta");
593 let mut layout = layout_before.clone();
594 layout.splice_range(&lines_after, 40, &[], &[], 1..2);
595
596 let fresh = WordWrapLayout::compute(&lines_after, 40, &[], &[]);
597 assert_eq!(layout.visual_lines(), fresh.visual_lines());
598 assert_eq!(layout.row_starts, fresh.row_starts);
599 }
600
601 #[test]
602 fn splice_range_handles_wrap_count_change() {
603 let lines_before = ls("short\ntail");
605 let mut layout = WordWrapLayout::compute(&lines_before, 10, &[], &[]);
606 let lines_after = ls("a very long line that will wrap\ntail");
607 layout.splice_range(&lines_after, 10, &[], &[], 0..1);
608
609 let fresh = WordWrapLayout::compute(&lines_after, 10, &[], &[]);
610 assert_eq!(layout.visual_lines(), fresh.visual_lines());
611 assert_eq!(layout.row_starts, fresh.row_starts);
612 }
613
614 #[test]
615 fn splice_range_at_buffer_start() {
616 let lines = ls("first line\nsecond line\nthird line");
617 let mut layout = WordWrapLayout::compute(&lines, 40, &[], &[]);
618 let edited = ls("first EDITED line\nsecond line\nthird line");
619 layout.splice_range(&edited, 40, &[], &[], 0..1);
620
621 let fresh = WordWrapLayout::compute(&edited, 40, &[], &[]);
622 assert_eq!(layout.visual_lines(), fresh.visual_lines());
623 assert_eq!(layout.row_starts, fresh.row_starts);
624 }
625
626 #[test]
627 fn splice_range_at_buffer_end() {
628 let lines = ls("first\nsecond\nthird");
629 let mut layout = WordWrapLayout::compute(&lines, 40, &[], &[]);
630 let edited = ls("first\nsecond\nthird EDITED");
631 layout.splice_range(&edited, 40, &[], &[], 2..3);
632
633 let fresh = WordWrapLayout::compute(&edited, 40, &[], &[]);
634 assert_eq!(layout.visual_lines(), fresh.visual_lines());
635 assert_eq!(layout.row_starts, fresh.row_starts);
636 }
637}