1#![allow(clippy::too_many_lines)]
14#![allow(clippy::redundant_closure_for_method_calls)]
16#![allow(clippy::option_if_let_else)]
18
19use crate::buffer::OptimizedBuffer;
20use crate::cell::{Cell, CellContent, GraphemeId};
21use crate::color::Rgba;
22use crate::style::Style;
23use crate::text::TextBuffer;
24use crate::unicode::{display_width_char_with_method, display_width_with_method};
25use std::cell::RefCell;
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum WrapMode {
30 #[default]
32 None,
33 Char,
35 Word,
37}
38
39#[derive(Clone, Copy, Debug, Default)]
41pub struct Viewport {
42 pub x: u32,
43 pub y: u32,
44 pub width: u32,
45 pub height: u32,
46}
47
48impl Viewport {
49 #[must_use]
51 pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
52 Self {
53 x,
54 y,
55 width,
56 height,
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default)]
63pub struct Selection {
64 pub start: usize,
65 pub end: usize,
66 pub style: Style,
67}
68
69impl Selection {
70 #[must_use]
72 pub fn new(start: usize, end: usize, style: Style) -> Self {
73 Self { start, end, style }
74 }
75
76 #[must_use]
78 pub fn is_empty(&self) -> bool {
79 self.start == self.end
80 }
81
82 #[must_use]
84 pub fn normalized(&self) -> Self {
85 if self.start <= self.end {
86 *self
87 } else {
88 Self {
89 start: self.end,
90 end: self.start,
91 style: self.style,
92 }
93 }
94 }
95
96 #[must_use]
98 pub fn contains(&self, pos: usize) -> bool {
99 let norm = self.normalized();
100 pos >= norm.start && pos < norm.end
101 }
102}
103
104#[derive(Clone, Copy, Debug, Default)]
106pub struct LocalSelection {
107 pub anchor_x: u32,
108 pub anchor_y: u32,
109 pub focus_x: u32,
110 pub focus_y: u32,
111 pub style: Style,
112}
113
114impl LocalSelection {
115 #[must_use]
117 pub fn new(anchor_x: u32, anchor_y: u32, focus_x: u32, focus_y: u32, style: Style) -> Self {
118 Self {
119 anchor_x,
120 anchor_y,
121 focus_x,
122 focus_y,
123 style,
124 }
125 }
126
127 #[must_use]
129 pub fn normalized(&self) -> (u32, u32, u32, u32) {
130 let min_x = self.anchor_x.min(self.focus_x);
131 let max_x = self.anchor_x.max(self.focus_x);
132 let min_y = self.anchor_y.min(self.focus_y);
133 let max_y = self.anchor_y.max(self.focus_y);
134 (min_x, min_y, max_x, max_y)
135 }
136}
137
138pub struct TextBufferView<'a> {
140 buffer: &'a TextBuffer,
141 viewport: Viewport,
142 wrap_mode: WrapMode,
143 wrap_width: Option<u32>,
144 scroll_x: u32,
145 scroll_y: u32,
146 selection: Option<Selection>,
147 local_selection: Option<LocalSelection>,
148 tab_indicator: Option<char>,
149 tab_indicator_color: Rgba,
150 truncate: bool,
151 line_cache: RefCell<Option<LineCache>>,
152}
153
154#[derive(Clone, Debug)]
155struct VirtualLine {
156 source_line: usize,
157 byte_start: usize,
158 byte_end: usize,
159 width: usize,
160 is_wrap: bool,
161}
162
163#[derive(Clone, Debug, Default)]
165pub struct LineInfo {
166 pub starts: Vec<usize>,
168 pub ends: Vec<usize>,
170 pub widths: Vec<usize>,
172 pub sources: Vec<usize>,
174 pub wraps: Vec<bool>,
176 pub max_width: usize,
178}
179
180impl LineInfo {
181 #[must_use]
183 pub fn virtual_line_count(&self) -> usize {
184 self.starts.len()
185 }
186
187 #[must_use]
192 pub fn source_to_virtual(&self, source_line: usize) -> Option<usize> {
193 self.sources.iter().position(|&s| s == source_line)
194 }
195
196 #[must_use]
201 pub fn virtual_to_source(&self, virtual_line: usize) -> Option<usize> {
202 self.sources.get(virtual_line).copied()
203 }
204
205 #[must_use]
210 pub fn virtual_line_byte_range(&self, virtual_line: usize) -> Option<(usize, usize)> {
211 let start = *self.starts.get(virtual_line)?;
212 let end = *self.ends.get(virtual_line)?;
213 Some((start, end))
214 }
215
216 #[must_use]
218 pub fn virtual_line_width(&self, virtual_line: usize) -> Option<usize> {
219 self.widths.get(virtual_line).copied()
220 }
221
222 #[must_use]
224 pub fn is_continuation(&self, virtual_line: usize) -> Option<bool> {
225 self.wraps.get(virtual_line).copied()
226 }
227
228 #[must_use]
230 pub fn virtual_lines_for_source(&self, source_line: usize) -> usize {
231 self.sources.iter().filter(|&&s| s == source_line).count()
232 }
233
234 #[must_use]
236 pub fn max_source_line(&self) -> Option<usize> {
237 self.sources.iter().max().copied()
238 }
239}
240
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
243pub struct TextMeasure {
244 pub line_count: usize,
245 pub max_width: usize,
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249struct LineCacheKey {
250 wrap_mode: WrapMode,
251 wrap_width_override: Option<u32>,
252 viewport_width: u32,
253 tab_width: u8,
254 width_method: crate::unicode::WidthMethod,
255 buffer_revision: u64,
256}
257
258#[derive(Clone, Debug)]
259struct LineCache {
260 key: LineCacheKey,
261 virtual_lines: Vec<VirtualLine>,
262 info: LineInfo,
263}
264
265impl<'a> TextBufferView<'a> {
266 #[must_use]
268 pub fn new(buffer: &'a TextBuffer) -> Self {
269 Self {
270 buffer,
271 viewport: Viewport::default(),
272 wrap_mode: WrapMode::None,
273 wrap_width: None,
274 scroll_x: 0,
275 scroll_y: 0,
276 selection: None,
277 local_selection: None,
278 tab_indicator: None,
279 tab_indicator_color: Rgba::WHITE,
280 truncate: false,
281 line_cache: RefCell::new(None),
282 }
283 }
284
285 #[must_use]
287 pub fn viewport(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
288 self.viewport = Viewport::new(x, y, width, height);
289 self.clear_line_cache();
290 self
291 }
292
293 #[must_use]
295 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
296 self.wrap_mode = mode;
297 self.clear_line_cache();
298 self
299 }
300
301 #[must_use]
303 pub fn wrap_width(mut self, width: u32) -> Self {
304 self.wrap_width = Some(width);
305 self.clear_line_cache();
306 self
307 }
308
309 #[must_use]
311 pub fn scroll(mut self, x: u32, y: u32) -> Self {
312 self.scroll_x = x;
313 self.scroll_y = y;
314 self
315 }
316
317 #[must_use]
319 pub fn tab_indicator(mut self, ch: char, color: Rgba) -> Self {
320 self.tab_indicator = Some(ch);
321 self.tab_indicator_color = color;
322 self
323 }
324
325 #[must_use]
327 pub fn truncate(mut self, enabled: bool) -> Self {
328 self.truncate = enabled;
329 self
330 }
331
332 pub fn set_selection(&mut self, start: usize, end: usize, style: Style) {
334 self.selection = Some(Selection::new(start, end, style));
335 }
336
337 pub fn clear_selection(&mut self) {
339 self.selection = None;
340 }
341
342 pub fn set_local_selection(
344 &mut self,
345 anchor_x: u32,
346 anchor_y: u32,
347 focus_x: u32,
348 focus_y: u32,
349 style: Style,
350 ) {
351 self.local_selection = Some(LocalSelection::new(
352 anchor_x, anchor_y, focus_x, focus_y, style,
353 ));
354 }
355
356 pub fn clear_local_selection(&mut self) {
358 self.local_selection = None;
359 }
360
361 fn clear_line_cache(&self) {
362 self.line_cache.replace(None);
363 }
364
365 #[must_use]
367 pub fn selected_text(&self) -> Option<String> {
368 let sel = self.selection.as_ref()?.normalized();
369 if sel.is_empty() {
370 return None;
371 }
372
373 let max = self.buffer.len_chars();
374 let start = sel.start.min(max);
375 let end = sel.end.min(max);
376 if start >= end {
377 return None;
378 }
379 Some(self.buffer.rope().slice(start..end).to_string())
380 }
381
382 fn effective_wrap_width(&self) -> Option<usize> {
383 if self.wrap_mode == WrapMode::None || self.viewport.width == 0 {
384 return None;
385 }
386 let width = self.wrap_width.unwrap_or(self.viewport.width).max(1);
387 Some(width as usize)
388 }
389
390 fn effective_wrap_width_for(&self, width: Option<u32>) -> Option<usize> {
391 if self.wrap_mode == WrapMode::None {
392 return None;
393 }
394 let base_width = width.unwrap_or(self.viewport.width);
395 if base_width == 0 {
396 return None;
397 }
398 let width = self.wrap_width.unwrap_or(base_width).max(1);
399 Some(width as usize)
400 }
401
402 fn line_cache_key(&self) -> LineCacheKey {
403 LineCacheKey {
404 wrap_mode: self.wrap_mode,
405 wrap_width_override: self.wrap_width,
406 viewport_width: self.viewport.width,
407 tab_width: self.buffer.tab_width(),
408 width_method: self.buffer.width_method(),
409 buffer_revision: self.buffer.revision(),
410 }
411 }
412
413 fn line_cache(&self) -> std::cell::Ref<'_, LineCache> {
414 let key = self.line_cache_key();
415 let needs_refresh = self
416 .line_cache
417 .borrow()
418 .as_ref()
419 .is_none_or(|cache| cache.key != key);
420
421 if needs_refresh {
422 let virtual_lines = self.build_virtual_lines_for(self.effective_wrap_width());
423 let info = Self::line_info_from_virtual_lines(&virtual_lines);
424 *self.line_cache.borrow_mut() = Some(LineCache {
425 key,
426 virtual_lines,
427 info,
428 });
429 }
430
431 std::cell::Ref::map(self.line_cache.borrow(), |cache| {
432 cache.as_ref().expect("line cache should exist")
433 })
434 }
435
436 fn line_info_from_virtual_lines(virtual_lines: &[VirtualLine]) -> LineInfo {
437 let mut info = LineInfo::default();
438 for line in virtual_lines {
439 info.starts.push(line.byte_start);
440 info.ends.push(line.byte_end);
441 info.widths.push(line.width);
442 info.sources.push(line.source_line);
443 info.wraps.push(line.is_wrap);
444 info.max_width = info.max_width.max(line.width);
445 }
446 info
447 }
448
449 fn build_virtual_lines_for(&self, wrap_width: Option<usize>) -> Vec<VirtualLine> {
450 use unicode_segmentation::UnicodeSegmentation;
451
452 let mut lines = Vec::new();
453 let method = self.buffer.width_method();
454 let tab_width = self.buffer.tab_width().max(1) as usize;
455
456 for line_idx in 0..self.buffer.len_lines() {
457 let Some(line) = self.buffer.line(line_idx) else {
458 continue;
459 };
460 let line = line.trim_end_matches('\n').trim_end_matches('\r');
461
462 let line_start_char = self.buffer.rope().line_to_char(line_idx);
463 let line_start_byte = self.buffer.rope().char_to_byte(line_start_char);
464
465 if line.is_empty() {
466 lines.push(VirtualLine {
467 source_line: line_idx,
468 byte_start: line_start_byte,
469 byte_end: line_start_byte,
470 width: 0,
471 is_wrap: false,
472 });
473 continue;
474 }
475
476 let Some(wrap_width) = wrap_width else {
477 let width = display_width_with_method(line, method);
478 lines.push(VirtualLine {
479 source_line: line_idx,
480 byte_start: line_start_byte,
481 byte_end: line_start_byte + line.len(),
482 width,
483 is_wrap: false,
484 });
485 continue;
486 };
487
488 let graphemes: Vec<(usize, &str)> = line.grapheme_indices(true).collect();
489 let mut start_byte = 0usize;
490 let mut current_width = 0usize;
491 let mut last_break: Option<(usize, usize, usize)> = None; let mut i = 0usize;
493
494 while i < graphemes.len() {
495 let (byte_idx, grapheme) = graphemes[i];
496 if byte_idx < start_byte {
497 i += 1;
498 continue;
499 }
500
501 let g_width = if grapheme == "\t" {
502 let offset = current_width % tab_width;
503 tab_width - offset
504 } else {
505 display_width_with_method(grapheme, method)
506 };
507
508 let is_ws = grapheme.chars().all(|c| c.is_whitespace());
509 if self.wrap_mode == WrapMode::Word && is_ws {
510 last_break = Some((byte_idx + grapheme.len(), current_width + g_width, i + 1));
511 }
512
513 if current_width + g_width > wrap_width && current_width > 0 {
514 let (break_byte, break_width, break_index) = if self.wrap_mode == WrapMode::Word
515 {
516 last_break.unwrap_or((byte_idx, current_width, i))
517 } else {
518 (byte_idx, current_width, i)
519 };
520
521 lines.push(VirtualLine {
522 source_line: line_idx,
523 byte_start: line_start_byte + start_byte,
524 byte_end: line_start_byte + break_byte,
525 width: break_width,
526 is_wrap: start_byte > 0,
527 });
528
529 start_byte = break_byte;
530 current_width = 0;
531 last_break = None;
532 i = break_index;
533
534 if self.wrap_mode == WrapMode::Word {
535 while i < graphemes.len() {
536 let (b, g) = graphemes[i];
537 if b < start_byte {
538 i += 1;
539 continue;
540 }
541 if g.chars().all(|c| c.is_whitespace()) {
542 start_byte = b + g.len();
543 i += 1;
544 } else {
545 break;
546 }
547 }
548 }
549
550 continue;
551 }
552
553 current_width += g_width;
554 i += 1;
555 }
556
557 if start_byte <= line.len() {
558 lines.push(VirtualLine {
559 source_line: line_idx,
560 byte_start: line_start_byte + start_byte,
561 byte_end: line_start_byte + line.len(),
562 width: current_width,
563 is_wrap: start_byte > 0,
564 });
565 }
566 }
567
568 lines
569 }
570
571 #[must_use]
573 pub fn visual_position_for_offset(&self, char_offset: usize) -> (u32, u32) {
574 use unicode_segmentation::UnicodeSegmentation;
575
576 let rope = self.buffer.rope();
577 let byte_offset = rope.char_to_byte(char_offset);
578 let cache = self.line_cache();
579 let method = self.buffer.width_method();
580 let tab_width = self.buffer.tab_width().max(1) as usize;
581
582 for (row, vline) in cache.virtual_lines.iter().enumerate() {
583 let is_last_line = row == cache.virtual_lines.len() - 1;
584 if byte_offset < vline.byte_start {
585 return (row as u32, 0);
586 }
587 if byte_offset > vline.byte_end {
591 if !is_last_line {
592 continue;
593 }
594 } else if byte_offset == vline.byte_end && !is_last_line {
595 let next_vline = &cache.virtual_lines[row + 1];
596 if next_vline.source_line == vline.source_line {
597 continue;
599 }
600 }
602
603 let char_start = rope.byte_to_char(vline.byte_start);
604 let char_end = rope.byte_to_char(byte_offset);
605 let text = rope.slice(char_start..char_end).to_string();
606
607 let mut width = 0usize;
608 for grapheme in text.graphemes(true) {
609 if grapheme == "\t" {
610 let offset = width % tab_width;
611 width += tab_width - offset;
612 } else {
613 width += display_width_with_method(grapheme, method);
614 }
615 }
616
617 return (row as u32, width as u32);
618 }
619
620 (0, 0)
621 }
622
623 #[must_use]
625 pub fn virtual_line_count(&self) -> usize {
626 self.line_cache().virtual_lines.len()
627 }
628
629 #[must_use]
631 pub fn line_info(&self) -> LineInfo {
632 self.line_cache().info.clone()
633 }
634
635 #[must_use]
637 pub fn measure_for_dimensions(&self, width: u32, _height: u32) -> TextMeasure {
638 let wrap_width = self.effective_wrap_width_for(Some(width.max(1)));
639 let virtual_lines = self.build_virtual_lines_for(wrap_width);
640 let info = Self::line_info_from_virtual_lines(&virtual_lines);
641 TextMeasure {
642 line_count: virtual_lines.len(),
643 max_width: info.max_width,
644 }
645 }
646
647 pub fn render_to(&self, output: &mut OptimizedBuffer, dest_x: i32, dest_y: i32) {
663 self.render_impl(output, dest_x, dest_y, None);
664 }
665
666 pub fn render_to_with_pool(
693 &self,
694 output: &mut OptimizedBuffer,
695 pool: &mut crate::grapheme_pool::GraphemePool,
696 dest_x: i32,
697 dest_y: i32,
698 ) {
699 self.render_impl(output, dest_x, dest_y, Some(pool));
700 }
701
702 fn render_impl(
703 &self,
704 output: &mut OptimizedBuffer,
705 dest_x: i32,
706 dest_y: i32,
707 mut pool: Option<&mut crate::grapheme_pool::GraphemePool>,
708 ) {
709 let cache = self.line_cache();
710 let virtual_lines = &cache.virtual_lines;
711 let start_line = self.scroll_y as usize;
712 let end_line = (start_line + self.viewport.height as usize).min(virtual_lines.len());
713
714 for (row_offset, vline_idx) in (start_line..end_line).enumerate() {
715 let vline = &virtual_lines[vline_idx];
716 let dest_row = dest_y + row_offset as i32;
717 if dest_row < 0 {
718 continue;
719 }
720 self.render_virtual_line(
727 output,
728 dest_x,
729 dest_row as u32,
730 vline,
731 row_offset as u32,
732 pool.as_deref_mut(),
733 );
734 }
735 }
736
737 fn render_virtual_line(
738 &self,
739 output: &mut OptimizedBuffer,
740 dest_x: i32,
741 dest_y: u32,
742 vline: &VirtualLine,
743 view_row: u32,
744 mut pool: Option<&mut crate::grapheme_pool::GraphemePool>,
745 ) {
746 use unicode_segmentation::UnicodeSegmentation;
747
748 let rope = self.buffer.rope();
749 let char_start = rope.byte_to_char(vline.byte_start);
750 let char_end = rope.byte_to_char(vline.byte_end);
751 let line = rope.slice(char_start..char_end).to_string();
752
753 let mut col = 0u32;
754 let method = self.buffer.width_method();
755
756 let selection = self.selection.as_ref().map(Selection::normalized);
757 let local_sel = self.local_selection;
758
759 let max_col = self.scroll_x + self.viewport.width;
760
761 let mut global_char_offset = char_start;
762 for grapheme in line.graphemes(true) {
763 if col >= max_col {
765 break;
766 }
767
768 if grapheme == "\t" {
769 let tab_width = self.buffer.tab_width().max(1) as u32;
770 let spaces_to_next = tab_width - (col % tab_width);
771 let byte_offset = rope.char_to_byte(global_char_offset);
773 let base_style = self.buffer.style_at(byte_offset);
774
775 for space_idx in 0..spaces_to_next {
776 if col < self.scroll_x {
778 col += 1;
779 continue;
780 }
781 if col >= max_col {
783 break;
784 }
785
786 let screen_col = (col as i32 - self.scroll_x as i32) + dest_x;
787 if screen_col >= 0 {
788 if space_idx == 0 {
789 if let Some(indicator) = self.tab_indicator {
790 let style = base_style.with_fg(self.tab_indicator_color);
792 output.set(screen_col as u32, dest_y, Cell::new(indicator, style));
793 } else {
794 output.set(screen_col as u32, dest_y, Cell::new(' ', base_style));
795 }
796 } else {
797 output.set(screen_col as u32, dest_y, Cell::new(' ', base_style));
798 }
799
800 if let Some(sel) = selection {
801 if sel.contains(global_char_offset) {
802 if let Some(cell) = output.get_mut(screen_col as u32, dest_y) {
803 cell.apply_style(sel.style);
804 }
805 }
806 }
807 if let Some(local) = local_sel {
808 let (min_x, min_y, max_x, max_y) = local.normalized();
809 let view_col = (screen_col - dest_x) as u32;
810 if view_col >= min_x
811 && view_col <= max_x
812 && view_row >= min_y
813 && view_row <= max_y
814 {
815 if let Some(cell) = output.get_mut(screen_col as u32, dest_y) {
816 cell.apply_style(local.style);
817 }
818 }
819 }
820 }
821 col += 1;
822 }
823 global_char_offset += 1;
824 continue;
825 }
826
827 let byte_offset = rope.char_to_byte(global_char_offset);
828 let style = self.buffer.style_at(byte_offset);
829 let (content, width) = if grapheme.chars().count() == 1 {
830 let ch = grapheme.chars().next().unwrap();
831 let w = display_width_char_with_method(ch, method);
832 (CellContent::Char(ch), w)
833 } else {
834 let w = display_width_with_method(grapheme, method);
835 if let Some(pool) = &mut pool {
836 let id = pool.intern(grapheme);
837 (CellContent::Grapheme(id), w)
838 } else {
839 (CellContent::Grapheme(GraphemeId::placeholder(w as u8)), w)
840 }
841 };
842 let mut main_cell = Cell {
843 content,
844 fg: style.fg.unwrap_or(Rgba::WHITE),
845 bg: style.bg.unwrap_or(Rgba::TRANSPARENT),
846 attributes: style.attributes,
847 };
848
849 if col + (width as u32) <= self.scroll_x {
851 col += width as u32;
852 global_char_offset += grapheme.chars().count();
853 continue;
854 }
855
856 if let Some(sel) = selection {
858 if sel.contains(global_char_offset) {
859 main_cell.apply_style(sel.style);
860 }
861 }
862
863 let start_screen_col = (col as i32 - self.scroll_x as i32) + dest_x;
866
867 for i in 0..width {
868 let screen_col = start_screen_col + i as i32;
869
870 if screen_col >= 0 {
872 let mut cell = if i == 0 {
873 main_cell
874 } else {
875 let mut c = Cell::continuation(main_cell.bg);
877 c.fg = main_cell.fg;
878 c.attributes = main_cell.attributes;
879 c
880 };
881
882 if let Some(local) = local_sel {
884 let (min_x, min_y, max_x, max_y) = local.normalized();
885 let view_col = (screen_col - dest_x) as u32;
886 if view_col >= min_x
887 && view_col <= max_x
888 && view_row >= min_y
889 && view_row <= max_y
890 {
891 cell.apply_style(local.style);
892 }
893 }
894
895 output.set(screen_col as u32, dest_y, cell);
896 }
897 }
898
899 col += width as u32;
900 global_char_offset += grapheme.chars().count();
901 }
902
903 if self.truncate && self.wrap_mode == WrapMode::None {
904 let max_cols = self.viewport.width as i32;
905 if vline.width as i32 > max_cols && max_cols > 0 {
906 let ellipsis_col = dest_x + (max_cols - 1);
907 if ellipsis_col >= 0 {
908 output.set(
909 ellipsis_col as u32,
910 dest_y,
911 Cell::new('…', self.buffer.default_style()),
912 );
913 }
914 }
915 }
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 #![allow(clippy::uninlined_format_args)]
922 use super::*;
923
924 #[test]
925 fn test_view_basic() {
926 let buffer = TextBuffer::with_text("Hello\nWorld");
927 let view = TextBufferView::new(&buffer).viewport(0, 0, 80, 24);
928 assert_eq!(view.virtual_line_count(), 2);
929 }
930
931 #[test]
932 fn test_selection() {
933 let buffer = TextBuffer::with_text("Hello, World!");
934 let mut view = TextBufferView::new(&buffer);
935 view.set_selection(0, 5, Style::NONE);
936 assert_eq!(view.selected_text(), Some("Hello".to_string()));
937 }
938
939 #[test]
940 fn test_wrap_char_count() {
941 let buffer = TextBuffer::with_text("abcdefghijklmnopqrstuvwxyz");
942 let view = TextBufferView::new(&buffer)
943 .viewport(0, 0, 5, 10)
944 .wrap_mode(WrapMode::Char);
945 assert!(view.virtual_line_count() >= 5);
946 }
947
948 #[test]
949 fn test_line_info_basic_wrap() {
950 let buffer = TextBuffer::with_text("abcd");
951 let view = TextBufferView::new(&buffer)
952 .viewport(0, 0, 2, 10)
953 .wrap_mode(WrapMode::Char);
954
955 let info = view.line_info();
956 assert_eq!(info.starts, vec![0, 2]);
957 assert_eq!(info.ends, vec![2, 4]);
958 assert_eq!(info.widths, vec![2, 2]);
959 assert_eq!(info.sources, vec![0, 0]);
960 assert_eq!(info.wraps, vec![false, true]);
961 assert_eq!(info.max_width, 2);
962 }
963
964 #[test]
965 fn test_virtual_line_byte_range_last_line() {
966 eprintln!(
967 "[TEST] test_virtual_line_byte_range_last_line: Verifying byte range for last line"
968 );
969
970 let buffer = TextBuffer::with_text("Hello World");
971 let view = TextBufferView::new(&buffer)
972 .viewport(0, 0, 80, 24)
973 .wrap_mode(WrapMode::None);
974
975 let info = view.line_info();
976 eprintln!("[TEST] Virtual line count: {}", info.virtual_line_count());
977
978 let range = info.virtual_line_byte_range(0);
980 eprintln!("[TEST] Byte range for line 0: {range:?}");
981
982 assert_eq!(
983 range,
984 Some((0, 11)),
985 "Last line should have correct byte range (0, 11)"
986 );
987
988 let text = &buffer.to_string()[0..11];
990 eprintln!("[TEST] Text in range: {text:?}");
991 assert_eq!(text, "Hello World");
992
993 eprintln!("[TEST] PASS: Last line byte range is correct");
994 }
995
996 #[test]
997 fn test_virtual_line_byte_range_wrapped() {
998 eprintln!(
999 "[TEST] test_virtual_line_byte_range_wrapped: Verifying byte ranges with wrapping"
1000 );
1001
1002 let buffer = TextBuffer::with_text("abcdefgh");
1003 let view = TextBufferView::new(&buffer)
1004 .viewport(0, 0, 3, 10)
1005 .wrap_mode(WrapMode::Char);
1006
1007 let info = view.line_info();
1008 eprintln!("[TEST] Virtual line count: {}", info.virtual_line_count());
1009
1010 assert_eq!(info.virtual_line_count(), 3);
1012
1013 let range0 = info.virtual_line_byte_range(0);
1014 let range1 = info.virtual_line_byte_range(1);
1015 let range2 = info.virtual_line_byte_range(2);
1016
1017 eprintln!("[TEST] Line 0 range: {range0:?}");
1018 eprintln!("[TEST] Line 1 range: {range1:?}");
1019 eprintln!("[TEST] Line 2 range: {range2:?}");
1020
1021 assert_eq!(range0, Some((0, 3)), "First line: bytes 0-3");
1022 assert_eq!(range1, Some((3, 6)), "Second line: bytes 3-6");
1023 assert_eq!(range2, Some((6, 8)), "Last line: bytes 6-8 (not 6-6!)");
1024
1025 eprintln!("[TEST] PASS: Wrapped line byte ranges are correct");
1026 }
1027
1028 #[test]
1029 fn test_measure_for_dimensions() {
1030 let buffer = TextBuffer::with_text("abc\ndefgh");
1031 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1032 let measure = view.measure_for_dimensions(3, 10);
1033 assert_eq!(
1034 measure,
1035 TextMeasure {
1036 line_count: 3,
1037 max_width: 3
1038 }
1039 );
1040 }
1041
1042 #[test]
1043 fn test_measure_no_wrap() {
1044 eprintln!("[TEST] test_measure_no_wrap: Measuring without wrapping");
1045
1046 let buffer = TextBuffer::with_text("short\nmedium text\nvery long line of text here");
1047 eprintln!("[TEST] Buffer lines: 'short', 'medium text', 'very long line of text here'");
1048
1049 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::None);
1050 let measure = view.measure_for_dimensions(10, 10);
1051
1052 eprintln!("[TEST] With WrapMode::None, width=10:");
1053 eprintln!("[TEST] line_count = {}", measure.line_count);
1054 eprintln!("[TEST] max_width = {}", measure.max_width);
1055
1056 assert_eq!(
1058 measure.line_count, 3,
1059 "Should have 3 source lines without wrapping"
1060 );
1061 assert_eq!(
1063 measure.max_width, 27,
1064 "Max width should be longest line (27 chars)"
1065 );
1066
1067 eprintln!("[TEST] PASS: No-wrap measurement correct");
1068 }
1069
1070 #[test]
1071 fn test_measure_with_char_wrap() {
1072 eprintln!("[TEST] test_measure_with_char_wrap: Measuring with character wrapping");
1073
1074 let buffer = TextBuffer::with_text("abcdefghij");
1075 eprintln!("[TEST] Buffer: 'abcdefghij' (10 chars)");
1076
1077 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1078
1079 let measure = view.measure_for_dimensions(3, 10);
1081 eprintln!("[TEST] With width=3, char wrap:");
1082 eprintln!(
1083 "[TEST] line_count = {} (expected 4: 'abc', 'def', 'ghi', 'j')",
1084 measure.line_count
1085 );
1086 eprintln!("[TEST] max_width = {}", measure.max_width);
1087
1088 assert_eq!(measure.line_count, 4, "10 chars / 3 = 4 wrapped lines");
1089 assert_eq!(measure.max_width, 3, "Max width capped at wrap width");
1090
1091 let measure2 = view.measure_for_dimensions(5, 10);
1093 eprintln!("[TEST] With width=5:");
1094 eprintln!(
1095 "[TEST] line_count = {} (expected 2: 'abcde', 'fghij')",
1096 measure2.line_count
1097 );
1098
1099 assert_eq!(measure2.line_count, 2, "10 chars / 5 = 2 wrapped lines");
1100 assert_eq!(measure2.max_width, 5, "Max width capped at wrap width");
1101
1102 eprintln!("[TEST] PASS: Char wrap measurement correct");
1103 }
1104
1105 #[test]
1106 fn test_measure_with_word_wrap() {
1107 eprintln!("[TEST] test_measure_with_word_wrap: Measuring with word wrapping");
1108
1109 let buffer = TextBuffer::with_text("hello world test");
1110 eprintln!("[TEST] Buffer: 'hello world test' (16 chars)");
1111
1112 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Word);
1113
1114 let measure = view.measure_for_dimensions(12, 10);
1116 eprintln!("[TEST] With width=12, word wrap:");
1117 eprintln!("[TEST] line_count = {}", measure.line_count);
1118 eprintln!("[TEST] max_width = {}", measure.max_width);
1119
1120 assert_eq!(measure.line_count, 2, "Should wrap to 2 lines at width 12");
1121 assert!(
1122 measure.max_width <= 12,
1123 "Max width should not exceed wrap width"
1124 );
1125
1126 let measure2 = view.measure_for_dimensions(6, 10);
1128 eprintln!("[TEST] With width=6:");
1129 eprintln!("[TEST] line_count = {}", measure2.line_count);
1130
1131 assert_eq!(measure2.line_count, 3, "Should wrap to 3 lines at width 6");
1132
1133 eprintln!("[TEST] PASS: Word wrap measurement correct");
1134 }
1135
1136 #[test]
1137 fn test_measure_empty_buffer() {
1138 eprintln!("[TEST] test_measure_empty_buffer: Measuring empty buffer");
1139
1140 let buffer = TextBuffer::new();
1141 eprintln!("[TEST] Empty buffer created");
1142
1143 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1144 let measure = view.measure_for_dimensions(80, 24);
1145
1146 eprintln!("[TEST] Measure results:");
1147 eprintln!("[TEST] line_count = {}", measure.line_count);
1148 eprintln!("[TEST] max_width = {}", measure.max_width);
1149
1150 assert!(
1152 measure.line_count <= 1,
1153 "Empty buffer should have 0 or 1 line"
1154 );
1155 assert_eq!(measure.max_width, 0, "Empty buffer should have max_width 0");
1156
1157 eprintln!("[TEST] PASS: Empty buffer measurement correct");
1158 }
1159
1160 #[test]
1161 fn test_measure_single_long_line() {
1162 eprintln!("[TEST] test_measure_single_long_line: Measuring single long line");
1163
1164 let long_line = "x".repeat(100);
1166 let buffer = TextBuffer::with_text(&long_line);
1167 eprintln!("[TEST] Single line of 100 'x' characters");
1168
1169 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1170
1171 let measure = view.measure_for_dimensions(20, 10);
1173 eprintln!("[TEST] With width=20:");
1174 eprintln!("[TEST] line_count = {} (expected 5)", measure.line_count);
1175 eprintln!("[TEST] max_width = {}", measure.max_width);
1176
1177 assert_eq!(measure.line_count, 5, "100 chars / 20 = 5 wrapped lines");
1178 assert_eq!(measure.max_width, 20, "Max width should be 20");
1179
1180 let measure2 = view.measure_for_dimensions(33, 10);
1182 eprintln!("[TEST] With width=33:");
1183 eprintln!(
1184 "[TEST] line_count = {} (expected 4: 33+33+33+1)",
1185 measure2.line_count
1186 );
1187
1188 assert_eq!(measure2.line_count, 4, "100 chars / 33 = 4 wrapped lines");
1189
1190 eprintln!("[TEST] PASS: Single long line measurement correct");
1191 }
1192
1193 #[test]
1194 fn test_measure_cjk_content() {
1195 eprintln!("[TEST] test_measure_cjk_content: Measuring CJK wide characters");
1196
1197 let buffer = TextBuffer::with_text("你好世界"); eprintln!("[TEST] Buffer: '你好世界' (4 CJK chars, ~8 display columns)");
1200
1201 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1202
1203 let measure = view.measure_for_dimensions(4, 10);
1205 eprintln!("[TEST] With width=4:");
1206 eprintln!("[TEST] line_count = {}", measure.line_count);
1207 eprintln!("[TEST] max_width = {}", measure.max_width);
1208
1209 assert_eq!(
1211 measure.line_count, 2,
1212 "4 CJK chars at width 4 should be 2 lines"
1213 );
1214 assert_eq!(measure.max_width, 4, "Max width should be 4");
1215
1216 let measure2 = view.measure_for_dimensions(8, 10);
1218 eprintln!("[TEST] With width=8:");
1219 eprintln!("[TEST] line_count = {}", measure2.line_count);
1220
1221 assert_eq!(
1222 measure2.line_count, 1,
1223 "All CJK chars should fit at width 8"
1224 );
1225
1226 eprintln!("[TEST] PASS: CJK content measurement correct");
1227 }
1228
1229 #[test]
1230 fn test_measure_updates_after_edit() {
1231 eprintln!("[TEST] test_measure_updates_after_edit: Verifying measurement updates");
1232
1233 let mut buffer = TextBuffer::with_text("short");
1234 eprintln!("[TEST] Initial buffer: 'short'");
1235
1236 let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1237 let measure1 = view.measure_for_dimensions(10, 10);
1238 eprintln!(
1239 "[TEST] Initial measure: line_count={}, max_width={}",
1240 measure1.line_count, measure1.max_width
1241 );
1242
1243 assert_eq!(measure1.line_count, 1);
1244 assert_eq!(measure1.max_width, 5);
1245
1246 buffer.set_text("this is a much longer line now");
1248 eprintln!("[TEST] Updated buffer: 'this is a much longer line now'");
1249
1250 let view2 = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1252 let measure2 = view2.measure_for_dimensions(10, 10);
1253 eprintln!(
1254 "[TEST] Updated measure: line_count={}, max_width={}",
1255 measure2.line_count, measure2.max_width
1256 );
1257
1258 assert_eq!(
1260 measure2.line_count, 3,
1261 "30 chars at width 10 should be 3 lines"
1262 );
1263 assert_eq!(measure2.max_width, 10);
1264
1265 eprintln!("[TEST] PASS: Measurement updates correctly after edit");
1266 }
1267
1268 #[test]
1269 fn test_measure_consistency_with_render() {
1270 use crate::buffer::OptimizedBuffer;
1271
1272 eprintln!("[TEST] test_measure_consistency_with_render: Comparing measure with render");
1273
1274 let buffer = TextBuffer::with_text("line1\nline2 is longer\nshort");
1275 eprintln!("[TEST] Buffer with 3 lines of varying length");
1276
1277 let view = TextBufferView::new(&buffer)
1278 .viewport(0, 0, 8, 10)
1279 .wrap_mode(WrapMode::Char);
1280
1281 let measure = view.measure_for_dimensions(8, 10);
1282 eprintln!(
1283 "[TEST] Measure: line_count={}, max_width={}",
1284 measure.line_count, measure.max_width
1285 );
1286
1287 let mut output = OptimizedBuffer::new(8, 10);
1289 view.render_to(&mut output, 0, 0);
1290
1291 let virtual_count = view.virtual_line_count();
1293 eprintln!("[TEST] virtual_line_count() = {virtual_count}");
1294
1295 assert_eq!(
1297 measure.line_count, virtual_count,
1298 "measure_for_dimensions line_count should match virtual_line_count"
1299 );
1300
1301 eprintln!("[TEST] PASS: Measurement consistent with render");
1302 }
1303
1304 #[test]
1305 fn test_tab_rendering_preserves_style() {
1306 use crate::buffer::OptimizedBuffer;
1307 use crate::cell::CellContent;
1308 use crate::color::Rgba;
1309 use crate::text::segment::StyledChunk;
1310
1311 eprintln!("[TEST] test_tab_rendering_preserves_style: Verifying TAB gets syntax style");
1312
1313 let mut buffer = TextBuffer::new();
1315 buffer.set_styled_text(&[
1316 StyledChunk::new("hello", Style::fg(Rgba::RED)),
1317 StyledChunk::new("\t", Style::fg(Rgba::GREEN)), StyledChunk::new("world", Style::fg(Rgba::BLUE)),
1319 ]);
1320 eprintln!("[TEST] Buffer text: {:?}", buffer.to_string());
1321
1322 let view = TextBufferView::new(&buffer).viewport(0, 0, 80, 24);
1323
1324 let mut output = OptimizedBuffer::new(80, 24);
1326 view.render_to(&mut output, 0, 0);
1327
1328 let cell_at_tab = output.get(5, 0);
1331 eprintln!("[TEST] Cell at tab position (5,0): {cell_at_tab:?}");
1332
1333 assert!(cell_at_tab.is_some(), "Cell at tab position should exist");
1335 let cell = cell_at_tab.unwrap();
1336 eprintln!("[TEST] Tab cell foreground: {:?}", cell.fg);
1338 assert!(
1340 matches!(cell.content, CellContent::Char(' ')),
1341 "Tab should render as space by default"
1342 );
1343 assert_eq!(
1344 cell.fg,
1345 Rgba::GREEN,
1346 "Tab should preserve syntax highlighting (GREEN)"
1347 );
1348
1349 let cell_at_world = output.get(8, 0); eprintln!("[TEST] Cell at 'world' start (8,0): {cell_at_world:?}");
1352 if let Some(cell) = cell_at_world {
1353 assert!(matches!(cell.content, CellContent::Char('w')));
1354 assert_eq!(cell.fg, Rgba::BLUE);
1355 }
1356
1357 eprintln!("[TEST] SUCCESS: Tab rendering preserves syntax highlighting");
1358 }
1359
1360 #[test]
1361 fn test_tab_indicator_with_style() {
1362 use crate::buffer::OptimizedBuffer;
1363 use crate::cell::CellContent;
1364 use crate::color::Rgba;
1365 use crate::text::segment::StyledChunk;
1366
1367 eprintln!("[TEST] test_tab_indicator_with_style: Tab indicator overrides fg, preserves bg");
1368
1369 let magenta = Rgba::rgb(1.0, 0.0, 1.0); let yellow = Rgba::rgb(1.0, 1.0, 0.0); let mut buffer = TextBuffer::new();
1375 let bg_style = Style::NONE.with_bg(magenta).with_fg(Rgba::GREEN);
1376 buffer.set_styled_text(&[
1377 StyledChunk::new("x", Style::NONE),
1378 StyledChunk::new("\t", bg_style), StyledChunk::new("y", Style::NONE),
1380 ]);
1381 eprintln!("[TEST] Buffer text: {:?}", buffer.to_string());
1382
1383 let view = TextBufferView::new(&buffer)
1385 .viewport(0, 0, 80, 24)
1386 .tab_indicator('→', yellow);
1387
1388 let mut output = OptimizedBuffer::new(80, 24);
1389 view.render_to(&mut output, 0, 0);
1390
1391 let cell = output.get(1, 0).expect("Cell should exist");
1393 eprintln!(
1394 "[TEST] Tab indicator cell: content={:?}, fg={:?}, bg={:?}",
1395 cell.content, cell.fg, cell.bg
1396 );
1397
1398 assert!(
1399 matches!(cell.content, CellContent::Char('→')),
1400 "Tab indicator should be arrow"
1401 );
1402 assert_eq!(cell.fg, yellow, "Tab indicator should have yellow fg");
1403 assert_eq!(
1405 cell.bg, magenta,
1406 "Tab should preserve background from syntax"
1407 );
1408
1409 eprintln!("[TEST] SUCCESS: Tab indicator correctly overrides fg while preserving bg");
1410 }
1411
1412 #[test]
1413 fn test_tab_expands_correctly() {
1414 use crate::buffer::OptimizedBuffer;
1415 use crate::cell::CellContent;
1416
1417 eprintln!("[TEST] test_tab_expands_correctly: Verifying tab expansion width");
1418
1419 let buffer = TextBuffer::with_text("ab\tcd");
1420 let view = TextBufferView::new(&buffer).viewport(0, 0, 80, 24);
1421
1422 let mut output = OptimizedBuffer::new(80, 24);
1423 view.render_to(&mut output, 0, 0);
1424
1425 eprintln!("[TEST] Checking character positions after tab expansion");
1430
1431 let cell_a = output.get(0, 0).expect("Cell should exist");
1432 assert!(matches!(cell_a.content, CellContent::Char('a')));
1433 eprintln!("[TEST] Position 0: {:?}", cell_a.content);
1434
1435 let cell_b = output.get(1, 0).expect("Cell should exist");
1436 assert!(matches!(cell_b.content, CellContent::Char('b')));
1437 eprintln!("[TEST] Position 1: {:?}", cell_b.content);
1438
1439 let cell_tab = output.get(2, 0).expect("Cell should exist");
1441 assert!(
1442 matches!(cell_tab.content, CellContent::Char(' ')),
1443 "Tab should expand to space"
1444 );
1445 eprintln!("[TEST] Position 2: {:?} (tab space)", cell_tab.content);
1446
1447 let cell_tab2 = output.get(3, 0).expect("Cell should exist");
1448 assert!(
1449 matches!(cell_tab2.content, CellContent::Char(' ')),
1450 "Tab should expand to space"
1451 );
1452 eprintln!("[TEST] Position 3: {:?} (tab space)", cell_tab2.content);
1453
1454 let cell_c = output.get(4, 0).expect("Cell should exist");
1456 assert!(matches!(cell_c.content, CellContent::Char('c')));
1457 eprintln!("[TEST] Position 4: {:?}", cell_c.content);
1458
1459 let cell_d = output.get(5, 0).expect("Cell should exist");
1460 assert!(matches!(cell_d.content, CellContent::Char('d')));
1461 eprintln!("[TEST] Position 5: {:?}", cell_d.content);
1462
1463 eprintln!("[TEST] SUCCESS: Tab expansion width is correct");
1464 }
1465
1466 #[test]
1467 fn test_tab_selection_highlights_all_columns() {
1468 use crate::buffer::OptimizedBuffer;
1469 use crate::cell::CellContent;
1470 use crate::color::Rgba;
1471
1472 eprintln!(
1473 "[TEST] test_tab_selection_highlights_all_columns: Verifying all tab columns get selection style (bd-nyo9)"
1474 );
1475
1476 let buffer = TextBuffer::with_text("ab\tcd");
1479 let selection_bg = Rgba::rgb(0.0, 0.0, 1.0); let selection_style = Style::NONE.with_bg(selection_bg);
1481
1482 let mut view = TextBufferView::new(&buffer).viewport(0, 0, 80, 24);
1483
1484 view.set_selection(2, 3, selection_style);
1486
1487 let mut output = OptimizedBuffer::new(80, 24);
1488 view.render_to(&mut output, 0, 0);
1489
1490 eprintln!("[TEST] Checking all tab columns have selection style");
1491
1492 for pos in 2..4 {
1495 let cell = output.get(pos, 0).expect("Cell should exist");
1496 eprintln!(
1497 "[TEST] Position {}: content={:?}, bg={:?}",
1498 pos, cell.content, cell.bg
1499 );
1500 assert!(
1501 matches!(cell.content, CellContent::Char(' ')),
1502 "Position {} should be space from tab expansion",
1503 pos
1504 );
1505 assert_eq!(
1506 cell.bg, selection_bg,
1507 "Position {} should have selection background (all tab columns should be highlighted)",
1508 pos
1509 );
1510 }
1511
1512 let cell_b = output.get(1, 0).expect("Cell should exist");
1514 assert_ne!(
1515 cell_b.bg, selection_bg,
1516 "Character before tab should not be selected"
1517 );
1518
1519 let cell_c = output.get(4, 0).expect("Cell should exist");
1520 assert_ne!(
1521 cell_c.bg, selection_bg,
1522 "Character after tab should not be selected"
1523 );
1524
1525 eprintln!("[TEST] SUCCESS: All tab columns correctly show selection style");
1526 }
1527
1528 #[test]
1531 fn test_line_cache_no_wrap() {
1532 eprintln!("[TEST] test_line_cache_no_wrap: Testing line cache without wrapping");
1533
1534 let buffer = TextBuffer::with_text("Hello World\nSecond Line\nThird");
1535 eprintln!("[TEST] Input text: {:?}", buffer.to_string());
1536 eprintln!("[TEST] Logical line count: {}", buffer.len_lines());
1537
1538 let view = TextBufferView::new(&buffer)
1539 .viewport(0, 0, 80, 24)
1540 .wrap_mode(WrapMode::None);
1541
1542 let info = view.line_info();
1543 eprintln!("[TEST] LineInfo results:");
1544 eprintln!("[TEST] virtual_line_count: {}", info.virtual_line_count());
1545 eprintln!("[TEST] max_width: {}", info.max_width);
1546
1547 for i in 0..info.virtual_line_count() {
1548 eprintln!(
1549 "[TEST] Line {}: start={} width={} source={} wrap={}",
1550 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1551 );
1552 }
1553
1554 assert_eq!(info.virtual_line_count(), 3, "Should have 3 virtual lines");
1555 assert_eq!(
1556 info.sources,
1557 vec![0, 1, 2],
1558 "Each virtual line maps to its source"
1559 );
1560 assert_eq!(info.wraps, vec![false, false, false], "No wrapping");
1561 assert_eq!(info.max_width, 11, "Max width should be 'Hello World' = 11");
1562
1563 eprintln!("[TEST] PASS: No-wrap mode produces correct line info");
1564 }
1565
1566 #[test]
1567 fn test_line_cache_char_wrap_exact() {
1568 eprintln!("[TEST] test_line_cache_char_wrap_exact: Testing char wrap at exact boundary");
1569
1570 let buffer = TextBuffer::with_text("abcdef");
1571 eprintln!(
1572 "[TEST] Input: {:?}, length: {}",
1573 buffer.to_string(),
1574 buffer.len_chars()
1575 );
1576
1577 let view = TextBufferView::new(&buffer)
1578 .viewport(0, 0, 3, 10)
1579 .wrap_mode(WrapMode::Char);
1580
1581 let info = view.line_info();
1582 eprintln!("[TEST] Wrap width: 3, LineInfo:");
1583 for i in 0..info.virtual_line_count() {
1584 eprintln!(
1585 "[TEST] Line {}: start={} width={} source={} wrap={}",
1586 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1587 );
1588 }
1589
1590 assert_eq!(info.virtual_line_count(), 2, "6 chars / 3 width = 2 lines");
1591 assert_eq!(info.widths, vec![3, 3], "Each line has width 3");
1592 assert_eq!(info.wraps, vec![false, true], "Second line is continuation");
1593
1594 eprintln!("[TEST] PASS: Char wrap at exact boundary works");
1595 }
1596
1597 #[test]
1598 fn test_line_cache_char_wrap_overflow() {
1599 eprintln!("[TEST] test_line_cache_char_wrap_overflow: Testing char wrap with overflow");
1600
1601 let buffer = TextBuffer::with_text("abcdefgh");
1602 eprintln!(
1603 "[TEST] Input: {:?}, length: {}",
1604 buffer.to_string(),
1605 buffer.len_chars()
1606 );
1607
1608 let view = TextBufferView::new(&buffer)
1609 .viewport(0, 0, 3, 10)
1610 .wrap_mode(WrapMode::Char);
1611
1612 let info = view.line_info();
1613 eprintln!("[TEST] Wrap width: 3, LineInfo:");
1614 for i in 0..info.virtual_line_count() {
1615 eprintln!(
1616 "[TEST] Line {}: start={} width={} source={} wrap={}",
1617 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1618 );
1619 }
1620
1621 assert_eq!(info.virtual_line_count(), 3, "8 chars / 3 width = 3 lines");
1622 assert_eq!(info.widths, vec![3, 3, 2], "Last line has 2 chars");
1623
1624 eprintln!("[TEST] PASS: Char wrap overflow works correctly");
1625 }
1626
1627 #[test]
1628 fn test_line_cache_word_wrap_simple() {
1629 eprintln!("[TEST] test_line_cache_word_wrap_simple: Testing word wrap");
1630
1631 let buffer = TextBuffer::with_text("Hello world test");
1632 eprintln!("[TEST] Input: {:?}", buffer.to_string());
1633 eprintln!("[TEST] Wrap width: 10");
1634
1635 let view = TextBufferView::new(&buffer)
1636 .viewport(0, 0, 10, 10)
1637 .wrap_mode(WrapMode::Word);
1638
1639 let info = view.line_info();
1640 eprintln!("[TEST] LineInfo:");
1641 for i in 0..info.virtual_line_count() {
1642 eprintln!(
1643 "[TEST] Line {}: start={} width={} source={} wrap={}",
1644 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1645 );
1646 }
1647
1648 assert!(
1651 info.virtual_line_count() >= 2,
1652 "Should wrap into at least 2 lines"
1653 );
1654
1655 eprintln!("[TEST] PASS: Word wrap breaks at word boundaries");
1656 }
1657
1658 #[test]
1659 fn test_line_cache_word_wrap_long_word() {
1660 eprintln!("[TEST] test_line_cache_word_wrap_long_word: Testing word wrap with long word");
1661
1662 let buffer = TextBuffer::with_text("supercalifragilisticexpialidocious");
1663 eprintln!(
1664 "[TEST] Input: {:?}, length: {}",
1665 buffer.to_string(),
1666 buffer.len_chars()
1667 );
1668
1669 let view = TextBufferView::new(&buffer)
1670 .viewport(0, 0, 10, 10)
1671 .wrap_mode(WrapMode::Word);
1672
1673 let info = view.line_info();
1674 eprintln!("[TEST] Wrap width: 10, LineInfo:");
1675 for i in 0..info.virtual_line_count() {
1676 eprintln!(
1677 "[TEST] Line {}: start={} width={} source={} wrap={}",
1678 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1679 );
1680 }
1681
1682 assert!(
1684 info.virtual_line_count() >= 3,
1685 "Long word should split across lines"
1686 );
1687
1688 eprintln!("[TEST] PASS: Long word breaks at character boundaries when no spaces");
1689 }
1690
1691 #[test]
1692 fn test_line_cache_multiple_lines() {
1693 eprintln!("[TEST] test_line_cache_multiple_lines: Testing multiple logical lines");
1694
1695 let buffer = TextBuffer::with_text("Short\nThis is longer\nEnd");
1696 eprintln!("[TEST] Input with 3 logical lines:");
1697 for (i, line) in buffer.to_string().lines().enumerate() {
1698 eprintln!("[TEST] Line {i}: {line:?}");
1699 }
1700
1701 let view = TextBufferView::new(&buffer)
1702 .viewport(0, 0, 10, 10)
1703 .wrap_mode(WrapMode::Word);
1704
1705 let info = view.line_info();
1706 eprintln!("[TEST] LineInfo (wrap_width=10):");
1707 for i in 0..info.virtual_line_count() {
1708 eprintln!(
1709 "[TEST] Virtual {}: start={} width={} source={} wrap={}",
1710 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1711 );
1712 }
1713
1714 assert!(info.virtual_line_count() > 3, "Middle line should wrap");
1716 assert_eq!(info.sources[0], 0, "First virtual line from source 0");
1717
1718 eprintln!("[TEST] PASS: Multiple lines with wrapping handled correctly");
1719 }
1720
1721 #[test]
1722 fn test_line_cache_empty_lines() {
1723 eprintln!("[TEST] test_line_cache_empty_lines: Testing empty lines");
1724
1725 let buffer = TextBuffer::with_text("Line1\n\nLine3");
1726 eprintln!("[TEST] Input: {:?}", buffer.to_string());
1727
1728 let view = TextBufferView::new(&buffer)
1729 .viewport(0, 0, 80, 24)
1730 .wrap_mode(WrapMode::None);
1731
1732 let info = view.line_info();
1733 eprintln!("[TEST] LineInfo:");
1734 for i in 0..info.virtual_line_count() {
1735 eprintln!(
1736 "[TEST] Line {}: start={} width={} source={} wrap={}",
1737 i, info.starts[i], info.widths[i], info.sources[i], info.wraps[i]
1738 );
1739 }
1740
1741 assert_eq!(
1742 info.virtual_line_count(),
1743 3,
1744 "Should have 3 lines including empty"
1745 );
1746 assert_eq!(info.widths[1], 0, "Empty line has width 0");
1747
1748 eprintln!("[TEST] PASS: Empty lines handled correctly");
1749 }
1750
1751 #[test]
1752 fn test_line_cache_utf8_width() {
1753 eprintln!("[TEST] test_line_cache_utf8_width: Testing UTF-8 character widths");
1754
1755 let buffer = TextBuffer::with_text("Hëllo");
1756 eprintln!(
1757 "[TEST] Input: {:?}, byte len: {}",
1758 buffer.to_string(),
1759 buffer.to_string().len()
1760 );
1761
1762 let view = TextBufferView::new(&buffer)
1763 .viewport(0, 0, 80, 24)
1764 .wrap_mode(WrapMode::None);
1765
1766 let info = view.line_info();
1767 eprintln!("[TEST] LineInfo:");
1768 eprintln!("[TEST] width: {}", info.widths[0]);
1769
1770 assert_eq!(info.widths[0], 5, "UTF-8 'ë' should have display width 1");
1771
1772 eprintln!("[TEST] PASS: UTF-8 characters have correct display width");
1773 }
1774
1775 #[test]
1776 fn test_line_cache_cjk_characters() {
1777 eprintln!("[TEST] test_line_cache_cjk_characters: Testing CJK character widths");
1778
1779 let buffer = TextBuffer::with_text("Hi中文Ok");
1781 eprintln!("[TEST] Input: {:?}", buffer.to_string());
1782 eprintln!("[TEST] Expected widths: H=1, i=1, 中=2, 文=2, O=1, k=1 = 8 total");
1783
1784 let view = TextBufferView::new(&buffer)
1785 .viewport(0, 0, 80, 24)
1786 .wrap_mode(WrapMode::None);
1787
1788 let info = view.line_info();
1789 eprintln!("[TEST] Computed width: {}", info.widths[0]);
1790
1791 assert_eq!(info.widths[0], 8, "CJK chars should be 2 columns each");
1792
1793 eprintln!("[TEST] PASS: CJK characters have width 2");
1794 }
1795
1796 #[test]
1797 fn test_line_cache_cjk_wrap() {
1798 eprintln!("[TEST] test_line_cache_cjk_wrap: Testing CJK wrapping doesn't break mid-char");
1799
1800 let buffer = TextBuffer::with_text("AB中文CD");
1801 eprintln!("[TEST] Input: {:?}", buffer.to_string());
1802 eprintln!("[TEST] Widths: A=1, B=1, 中=2, 文=2, C=1, D=1 = 8");
1803
1804 let view = TextBufferView::new(&buffer)
1805 .viewport(0, 0, 5, 10)
1806 .wrap_mode(WrapMode::Char);
1807
1808 let info = view.line_info();
1809 eprintln!("[TEST] Wrap width: 5, LineInfo:");
1810 for i in 0..info.virtual_line_count() {
1811 eprintln!("[TEST] Line {}: width={}", i, info.widths[i]);
1812 }
1813
1814 for (i, &width) in info.widths.iter().enumerate() {
1816 eprintln!("[TEST] Verifying line {i} width {width} <= 5");
1817 assert!(width <= 5, "Line {i} width {width} exceeds wrap width 5");
1818 }
1819
1820 eprintln!("[TEST] PASS: CJK characters not broken mid-character");
1821 }
1822
1823 #[test]
1824 fn test_line_cache_emoji_grapheme_clusters() {
1825 eprintln!("[TEST] test_line_cache_emoji_grapheme_clusters: Testing multi-codepoint emoji");
1826
1827 let buffer = TextBuffer::with_text("Hi👨\u{200D}👩\u{200D}👧Ok");
1830 eprintln!("[TEST] Input: 'Hi' + family emoji + 'Ok'");
1831 eprintln!("[TEST] Expected widths: H=1, i=1, family=2, O=1, k=1 = 6 total");
1832
1833 let view = TextBufferView::new(&buffer)
1834 .viewport(0, 0, 80, 24)
1835 .wrap_mode(WrapMode::None);
1836
1837 let info = view.line_info();
1838 eprintln!("[TEST] Computed width: {}", info.widths[0]);
1839
1840 assert_eq!(info.widths[0], 6, "Family emoji should be 2 columns");
1842
1843 eprintln!("[TEST] PASS: Multi-codepoint emoji width correct");
1844 }
1845
1846 #[test]
1847 fn test_line_cache_emoji_wrap() {
1848 eprintln!(
1849 "[TEST] test_line_cache_emoji_wrap: Testing emoji wrapping doesn't break mid-grapheme"
1850 );
1851
1852 let buffer = TextBuffer::with_text("AB👨\u{200D}👩\u{200D}👧CD");
1854 eprintln!("[TEST] Input: 'AB' + family emoji + 'CD'");
1855 eprintln!("[TEST] Widths: A=1, B=1, family=2, C=1, D=1 = 6 total");
1856
1857 let view = TextBufferView::new(&buffer)
1858 .viewport(0, 0, 3, 10)
1859 .wrap_mode(WrapMode::Char);
1860
1861 let info = view.line_info();
1862 eprintln!("[TEST] Wrap width: 3, LineInfo:");
1863 for i in 0..info.virtual_line_count() {
1864 eprintln!("[TEST] Line {}: width={}", i, info.widths[i]);
1865 }
1866
1867 for (i, &width) in info.widths.iter().enumerate() {
1873 eprintln!("[TEST] Verifying line {i} width {width} <= 3");
1874 assert!(width <= 3, "Line {i} width {width} exceeds wrap width 3");
1875 }
1876
1877 eprintln!("[TEST] PASS: Emoji grapheme clusters not broken mid-grapheme");
1878 }
1879
1880 #[test]
1881 fn test_line_cache_invalidation_content() {
1882 eprintln!("[TEST] test_line_cache_invalidation_content: Testing cache invalidation");
1883
1884 let buffer = TextBuffer::with_text("Hello");
1885 let view = TextBufferView::new(&buffer)
1886 .viewport(0, 0, 80, 24)
1887 .wrap_mode(WrapMode::None);
1888
1889 let info1 = view.line_info();
1890 eprintln!(
1891 "[TEST] Initial info: lines={}, max_width={}",
1892 info1.virtual_line_count(),
1893 info1.max_width
1894 );
1895
1896 let buffer2 = TextBuffer::with_text("Hello World Extended");
1898 let view2 = TextBufferView::new(&buffer2)
1899 .viewport(0, 0, 80, 24)
1900 .wrap_mode(WrapMode::None);
1901
1902 let info2 = view2.line_info();
1903 eprintln!(
1904 "[TEST] New info: lines={}, max_width={}",
1905 info2.virtual_line_count(),
1906 info2.max_width
1907 );
1908
1909 assert_ne!(
1910 info1.max_width, info2.max_width,
1911 "Different content should have different width"
1912 );
1913
1914 eprintln!("[TEST] PASS: Cache correctly reflects content changes");
1915 }
1916
1917 #[test]
1918 fn test_line_cache_invalidation_wrap_mode() {
1919 eprintln!("[TEST] test_line_cache_invalidation_wrap_mode: Testing wrap mode change");
1920
1921 let buffer = TextBuffer::with_text("Hello World Test Line");
1922
1923 let view_none = TextBufferView::new(&buffer)
1924 .viewport(0, 0, 10, 10)
1925 .wrap_mode(WrapMode::None);
1926 let info_none = view_none.line_info();
1927 eprintln!(
1928 "[TEST] WrapMode::None: lines={}",
1929 info_none.virtual_line_count()
1930 );
1931
1932 let view_char = TextBufferView::new(&buffer)
1933 .viewport(0, 0, 10, 10)
1934 .wrap_mode(WrapMode::Char);
1935 let info_char = view_char.line_info();
1936 eprintln!(
1937 "[TEST] WrapMode::Char: lines={}",
1938 info_char.virtual_line_count()
1939 );
1940
1941 assert_ne!(
1942 info_none.virtual_line_count(),
1943 info_char.virtual_line_count(),
1944 "Different wrap modes should produce different line counts"
1945 );
1946
1947 eprintln!("[TEST] PASS: Wrap mode change produces different results");
1948 }
1949
1950 #[test]
1951 fn test_source_to_virtual_mapping() {
1952 eprintln!("[TEST] test_source_to_virtual_mapping: Testing source -> virtual mapping");
1953
1954 let buffer = TextBuffer::with_text("Short\nThis is a longer line that wraps\nEnd");
1955 eprintln!("[TEST] Input with 3 logical lines");
1956
1957 let view = TextBufferView::new(&buffer)
1958 .viewport(0, 0, 15, 10)
1959 .wrap_mode(WrapMode::Word);
1960
1961 let info = view.line_info();
1962 eprintln!("[TEST] Virtual lines:");
1963 for i in 0..info.virtual_line_count() {
1964 eprintln!("[TEST] Virtual {}: source={}", i, info.sources[i]);
1965 }
1966
1967 for src in 0..=2 {
1969 let virt = info.source_to_virtual(src);
1970 eprintln!("[TEST] source_to_virtual({src}) = {virt:?}");
1971 assert!(virt.is_some(), "Source {src} should map to a virtual line");
1972 }
1973
1974 for virt in 0..info.virtual_line_count() {
1976 let src = info.virtual_to_source(virt);
1977 eprintln!("[TEST] virtual_to_source({virt}) = {src:?}");
1978 assert!(src.is_some(), "Virtual {virt} should map to a source line");
1979 }
1980
1981 for src in 0..=2 {
1983 if let Some(virt) = info.source_to_virtual(src) {
1984 let back = info.virtual_to_source(virt).unwrap();
1985 eprintln!("[TEST] Round-trip: {src} -> {virt} -> {back}");
1986 assert_eq!(back, src, "Round-trip should preserve source line");
1987 }
1988 }
1989
1990 eprintln!("[TEST] PASS: Source/virtual mappings are correct");
1991 }
1992
1993 #[test]
1994 fn test_virtual_to_source_mapping() {
1995 eprintln!("[TEST] test_virtual_to_source_mapping: Testing virtual -> source mapping");
1996
1997 let buffer = TextBuffer::with_text("Line one\nLine two\nLine three");
1998 let view = TextBufferView::new(&buffer)
1999 .viewport(0, 0, 5, 10)
2000 .wrap_mode(WrapMode::Char);
2001
2002 let info = view.line_info();
2003 eprintln!("[TEST] {} virtual lines", info.virtual_line_count());
2004
2005 for virt in 0..info.virtual_line_count() {
2006 let src = info.virtual_to_source(virt);
2007 let is_cont = info.is_continuation(virt);
2008 eprintln!("[TEST] Virtual {virt} -> source {src:?}, is_continuation: {is_cont:?}");
2009 }
2010
2011 let oob = info.virtual_to_source(1000);
2013 assert!(oob.is_none(), "Out of bounds should return None");
2014
2015 eprintln!("[TEST] PASS: Virtual to source mapping works");
2016 }
2017
2018 #[test]
2019 fn test_line_info_helper_methods() {
2020 eprintln!("[TEST] test_line_info_helper_methods: Testing LineInfo helper methods");
2021
2022 let buffer = TextBuffer::with_text("Hello\nWorld");
2023 let view = TextBufferView::new(&buffer)
2024 .viewport(0, 0, 80, 24)
2025 .wrap_mode(WrapMode::None);
2026
2027 let info = view.line_info();
2028
2029 eprintln!("[TEST] virtual_line_count: {}", info.virtual_line_count());
2030 assert_eq!(info.virtual_line_count(), 2);
2031
2032 eprintln!("[TEST] max_source_line: {:?}", info.max_source_line());
2033 assert_eq!(info.max_source_line(), Some(1));
2034
2035 eprintln!(
2036 "[TEST] virtual_lines_for_source(0): {}",
2037 info.virtual_lines_for_source(0)
2038 );
2039 assert_eq!(info.virtual_lines_for_source(0), 1);
2040
2041 eprintln!(
2042 "[TEST] virtual_line_width(0): {:?}",
2043 info.virtual_line_width(0)
2044 );
2045 assert_eq!(info.virtual_line_width(0), Some(5));
2046
2047 eprintln!("[TEST] is_continuation(0): {:?}", info.is_continuation(0));
2048 assert_eq!(info.is_continuation(0), Some(false));
2049
2050 eprintln!("[TEST] PASS: Helper methods work correctly");
2051 }
2052
2053 #[test]
2054 fn test_line_cache_performance() {
2055 use std::fmt::Write as _;
2056 use std::time::Instant;
2057
2058 eprintln!("[PERF] test_line_cache_performance: Testing cache performance");
2059
2060 let mut text = String::new();
2062 for i in 0..10_000 {
2063 let _ = writeln!(
2064 text,
2065 "Line {i} with some content that might wrap when narrow"
2066 );
2067 }
2068
2069 let buffer = TextBuffer::with_text(&text);
2070 eprintln!(
2071 "[PERF] Buffer size: {} bytes, {} lines",
2072 text.len(),
2073 buffer.len_lines()
2074 );
2075
2076 let view = TextBufferView::new(&buffer)
2077 .viewport(0, 0, 80, 100)
2078 .wrap_mode(WrapMode::Word);
2079
2080 let start = Instant::now();
2081 let info = view.line_info();
2082 let elapsed = start.elapsed();
2083
2084 eprintln!("[PERF] Cache computation time: {elapsed:?}");
2085 eprintln!("[PERF] Virtual lines: {}", info.virtual_line_count());
2086 eprintln!("[PERF] Max width: {}", info.max_width);
2087 let lines_per_ms = 10_000.0 / elapsed.as_secs_f64() / 1000.0;
2088 eprintln!("[PERF] Lines per millisecond: {lines_per_ms:.0}");
2089
2090 if elapsed.as_millis() > 10 {
2092 eprintln!("[PERF] WARNING: Took {elapsed:?}, expected <10ms");
2093 }
2094 assert!(
2095 elapsed.as_millis() < 150,
2096 "Cache computation took {elapsed:?}, should be <150ms"
2097 );
2098
2099 eprintln!("[PERF] PASS: 10K lines processed efficiently");
2100 }
2101
2102 #[test]
2103 fn test_render_emoji_with_pool() {
2104 use crate::buffer::OptimizedBuffer;
2105 use crate::cell::CellContent;
2106 use crate::grapheme_pool::GraphemePool;
2107
2108 let buffer = TextBuffer::with_text("👨👩👧");
2109 let view = TextBufferView::new(&buffer).viewport(0, 0, 10, 1);
2110 let mut output = OptimizedBuffer::new(10, 1);
2111 let mut pool = GraphemePool::new();
2112
2113 view.render_to_with_pool(&mut output, &mut pool, 0, 0);
2114
2115 let cell = output.get(0, 0).unwrap();
2116 if let CellContent::Grapheme(id) = cell.content {
2117 assert!(
2119 id.pool_id() > 0,
2120 "Expected valid pool ID for interned grapheme"
2121 );
2122 assert_eq!(id.width(), 2, "Width should be 2");
2123
2124 assert_eq!(pool.get(id), Some("👨👩👧"));
2126 } else {
2127 assert!(
2128 matches!(cell.content, CellContent::Grapheme(_)),
2129 "Expected Grapheme content"
2130 );
2131 }
2132 }
2133}