Skip to main content

opentui_rust/text/
view.rs

1//! Text buffer view with viewport and wrapping.
2//!
3//! # Grapheme Handling
4//!
5//! When rendering text that may contain multi-codepoint graphemes (emoji, ZWJ sequences,
6//! combining characters), use [`TextBufferView::render_to_with_pool`] to preserve the
7//! grapheme content. The simpler [`TextBufferView::render_to`] method creates placeholder
8//! cells that preserve display width but lose the actual grapheme string.
9//!
10//! See the method documentation for details on when to use each variant.
11
12// Complex rendering logic naturally has long functions
13#![allow(clippy::too_many_lines)]
14// Closures with method references are more readable in context
15#![allow(clippy::redundant_closure_for_method_calls)]
16// if-let-else is clearer than map_or_else for mutable pool reborrowing
17#![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/// Text wrapping mode.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum WrapMode {
30    /// No wrapping - lines extend beyond viewport.
31    #[default]
32    None,
33    /// Wrap at character boundaries.
34    Char,
35    /// Wrap at word boundaries.
36    Word,
37}
38
39/// Viewport configuration.
40#[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    /// Create a new viewport.
50    #[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/// Selection range.
62#[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    /// Create a new selection.
71    #[must_use]
72    pub fn new(start: usize, end: usize, style: Style) -> Self {
73        Self { start, end, style }
74    }
75
76    /// Check if empty.
77    #[must_use]
78    pub fn is_empty(&self) -> bool {
79        self.start == self.end
80    }
81
82    /// Get normalized (start <= end) selection.
83    #[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    /// Check if position is within selection.
97    #[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/// Local (viewport) selection based on screen coordinates.
105#[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    /// Create a new local selection.
116    #[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    /// Normalize selection rectangle.
128    #[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
138/// View into a text buffer with viewport and rendering options.
139pub 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/// Cached line layout information for wrapped text.
164#[derive(Clone, Debug, Default)]
165pub struct LineInfo {
166    /// Byte offset where each virtual line starts.
167    pub starts: Vec<usize>,
168    /// Byte offset where each virtual line ends (exclusive).
169    pub ends: Vec<usize>,
170    /// Display width of each virtual line.
171    pub widths: Vec<usize>,
172    /// Source line index for each virtual line.
173    pub sources: Vec<usize>,
174    /// Whether the line is a wrapped continuation.
175    pub wraps: Vec<bool>,
176    /// Maximum line width across all virtual lines.
177    pub max_width: usize,
178}
179
180impl LineInfo {
181    /// Get the number of virtual lines.
182    #[must_use]
183    pub fn virtual_line_count(&self) -> usize {
184        self.starts.len()
185    }
186
187    /// Map a source (logical) line to its first virtual line index.
188    ///
189    /// Returns the index of the first virtual line that corresponds to
190    /// the given source line, or `None` if the source line doesn't exist.
191    #[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    /// Map a virtual line index to its source (logical) line.
197    ///
198    /// Returns the source line index for the given virtual line,
199    /// or `None` if the virtual line index is out of bounds.
200    #[must_use]
201    pub fn virtual_to_source(&self, virtual_line: usize) -> Option<usize> {
202        self.sources.get(virtual_line).copied()
203    }
204
205    /// Get the byte range for a virtual line.
206    ///
207    /// Returns `(byte_start, byte_end)` for the given virtual line index,
208    /// or `None` if the index is out of bounds.
209    #[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    /// Get the display width of a virtual line.
217    #[must_use]
218    pub fn virtual_line_width(&self, virtual_line: usize) -> Option<usize> {
219        self.widths.get(virtual_line).copied()
220    }
221
222    /// Check if a virtual line is a wrapped continuation.
223    #[must_use]
224    pub fn is_continuation(&self, virtual_line: usize) -> Option<bool> {
225        self.wraps.get(virtual_line).copied()
226    }
227
228    /// Count virtual lines for a given source line.
229    #[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    /// Get the maximum source line index.
235    #[must_use]
236    pub fn max_source_line(&self) -> Option<usize> {
237        self.sources.iter().max().copied()
238    }
239}
240
241/// Measurement result for a given viewport size.
242#[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    /// Create a new view of a text buffer.
267    #[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    /// Set the viewport.
286    #[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    /// Set the wrap mode.
294    #[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    /// Set explicit wrap width (overrides viewport width when wrapping).
302    #[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    /// Set scroll position.
310    #[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    /// Set tab indicator character and color.
318    #[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    /// Enable or disable truncation.
326    #[must_use]
327    pub fn truncate(mut self, enabled: bool) -> Self {
328        self.truncate = enabled;
329        self
330    }
331
332    /// Set selection.
333    pub fn set_selection(&mut self, start: usize, end: usize, style: Style) {
334        self.selection = Some(Selection::new(start, end, style));
335    }
336
337    /// Clear selection.
338    pub fn clear_selection(&mut self) {
339        self.selection = None;
340    }
341
342    /// Set a local (viewport) selection.
343    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    /// Clear local selection.
357    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    /// Get selected text if any.
366    #[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; // (break_byte, width, index)
492            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    /// Compute visual (wrapped) position for a character offset.
572    #[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            // Check if cursor is within this line or at its end
588            // When byte_offset == byte_end (cursor at newline position), match this line
589            // if it's the last line OR the next line is on a different source line
590            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                    // Next line is a wrap continuation of same source line, skip
598                    continue;
599                }
600                // Next line is a new source line, cursor at end belongs here
601            }
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    /// Calculate the number of virtual lines (accounting for wrapping).
624    #[must_use]
625    pub fn virtual_line_count(&self) -> usize {
626        self.line_cache().virtual_lines.len()
627    }
628
629    /// Get line layout information for the current view.
630    #[must_use]
631    pub fn line_info(&self) -> LineInfo {
632        self.line_cache().info.clone()
633    }
634
635    /// Measure line count and max width for a given viewport size.
636    #[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    /// Render the view to an output buffer.
648    ///
649    /// # Grapheme Handling
650    ///
651    /// Multi-codepoint graphemes (emoji, ZWJ sequences, characters with combining marks)
652    /// are rendered as **placeholders** with only their display width preserved. The actual
653    /// grapheme content is lost because no [`GraphemePool`] is provided for interning.
654    ///
655    /// Use [`render_to_with_pool`] instead when:
656    /// - Rendering emoji or complex Unicode characters
657    /// - The output buffer will be converted to ANSI sequences for terminal display
658    /// - You need to recover the original grapheme strings later
659    ///
660    /// [`GraphemePool`]: crate::grapheme_pool::GraphemePool
661    /// [`render_to_with_pool`]: Self::render_to_with_pool
662    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    /// Render the view to an output buffer, interning complex graphemes in the pool.
667    ///
668    /// # When to Use This Method
669    ///
670    /// Use this method instead of [`render_to`] when your text contains:
671    /// - Emoji (e.g., 👨‍👩‍👧, 🎉)
672    /// - Characters with combining marks (e.g., é composed as e + ́)
673    /// - ZWJ (Zero-Width Joiner) sequences
674    /// - Any multi-codepoint grapheme clusters
675    ///
676    /// The provided [`GraphemePool`] interns these complex graphemes, allowing them
677    /// to be recovered later when generating ANSI output for terminal display.
678    ///
679    /// # Example
680    ///
681    /// ```ignore
682    /// use opentui_rust::grapheme_pool::GraphemePool;
683    ///
684    /// let mut pool = GraphemePool::new();
685    /// let mut output = OptimizedBuffer::new(80, 24);
686    /// view.render_to_with_pool(&mut output, &mut pool, 0, 0);
687    /// // Graphemes in output cells can now be resolved via pool.get(id)
688    /// ```
689    ///
690    /// [`render_to`]: Self::render_to
691    /// [`GraphemePool`]: crate::grapheme_pool::GraphemePool
692    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            // We need to re-borrow pool for each iteration if it exists
721            // Since Option<&mut T> is not Copy, we need a way to pass it.
722            // But we can't easily clone mutable ref.
723            // However, render_virtual_line works on one line.
724            // We can pass `as_deref_mut`? No, that consumes the option if we are not careful.
725            // Actually, we can just pass `pool.as_deref_mut()` to re-borrow.
726            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            // Optimization: Stop if we've gone past the viewport
764            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                // Get the actual style at this position (preserves syntax highlighting)
772                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                    // Optimization: Skip if before scroll position
777                    if col < self.scroll_x {
778                        col += 1;
779                        continue;
780                    }
781                    // Stop if we hit the edge (tab might straddle the edge)
782                    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                                // Tab indicator gets special foreground but preserves background
791                                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            // Optimization: Skip if completely before scroll position
850            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            // Apply global selection style once
857            if let Some(sel) = selection {
858                if sel.contains(global_char_offset) {
859                    main_cell.apply_style(sel.style);
860                }
861            }
862
863            // Draw parts (main + continuations)
864            // Use i32 to allow negative screen coordinates (off-left) without panic
865            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                // Check visibility for this specific column
871                if screen_col >= 0 {
872                    let mut cell = if i == 0 {
873                        main_cell
874                    } else {
875                        // Continuation cell - ensure it carries background/style
876                        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                    // Apply local selection per-column
883                    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        // Test the byte range for the last (and only) virtual line
979        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        // Verify the content matches
989        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        // Should wrap to 3 lines: "abc", "def", "gh"
1011        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        // Without wrapping, should have exactly 3 lines
1057        assert_eq!(
1058            measure.line_count, 3,
1059            "Should have 3 source lines without wrapping"
1060        );
1061        // Max width should be the longest line: "very long line of text here" = 27 chars
1062        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        // Wrap at width 3
1080        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        // Wrap at width 5
1092        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        // Wrap at width 12 - "hello world" fits (11), "test" on next line
1115        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        // Wrap at width 6 - each word should be on its own line
1127        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        // Empty buffer should have 0 or 1 line depending on implementation
1151        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        // Create a 100-character line
1165        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        // Wrap at width 20
1172        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        // Wrap at width 33
1181        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        // CJK characters are typically 2 columns wide
1198        let buffer = TextBuffer::with_text("你好世界"); // 4 CJK chars = 8 display columns
1199        eprintln!("[TEST] Buffer: '你好世界' (4 CJK chars, ~8 display columns)");
1200
1201        let view = TextBufferView::new(&buffer).wrap_mode(WrapMode::Char);
1202
1203        // With width 4, each CJK char is 2 wide, so 2 chars per line
1204        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        // Should wrap to 2 lines: "你好" (4 cols) and "世界" (4 cols)
1210        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        // With width 8, all 4 chars should fit on one line
1217        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        // Modify the buffer
1247        buffer.set_text("this is a much longer line now");
1248        eprintln!("[TEST] Updated buffer: 'this is a much longer line now'");
1249
1250        // Create new view with updated buffer
1251        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        // "this is a much longer line now" = 30 chars, at width 10 = 3 lines
1259        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        // Now render and count actual lines
1288        let mut output = OptimizedBuffer::new(8, 10);
1289        view.render_to(&mut output, 0, 0);
1290
1291        // Count rendered lines by checking for non-default content
1292        let virtual_count = view.virtual_line_count();
1293        eprintln!("[TEST] virtual_line_count() = {virtual_count}");
1294
1295        // Measure should match virtual line count
1296        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        // Create buffer with styled text containing a tab
1314        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)), // Tab with green style
1318            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        // Render to buffer
1325        let mut output = OptimizedBuffer::new(80, 24);
1326        view.render_to(&mut output, 0, 0);
1327
1328        // Check the cell at position where tab starts (after "hello")
1329        // Tab at position 5 should have the green style (from the styled chunk)
1330        let cell_at_tab = output.get(5, 0);
1331        eprintln!("[TEST] Cell at tab position (5,0): {cell_at_tab:?}");
1332
1333        // The tab should render as space(s) but preserve the GREEN style
1334        assert!(cell_at_tab.is_some(), "Cell at tab position should exist");
1335        let cell = cell_at_tab.unwrap();
1336        // The foreground should be GREEN since that's the style at the tab position
1337        eprintln!("[TEST] Tab cell foreground: {:?}", cell.fg);
1338        // Note: without tab_indicator set, the cell is a space with the style from style_at
1339        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        // Verify "world" has blue style
1350        let cell_at_world = output.get(8, 0); // Tab expands to position 8
1351        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        // Define test colors
1370        let magenta = Rgba::rgb(1.0, 0.0, 1.0); // Magenta: full red + blue
1371        let yellow = Rgba::rgb(1.0, 1.0, 0.0); // Yellow: full red + green
1372
1373        // Create buffer with styled text containing a tab with background
1374        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), // Tab with magenta background
1379            StyledChunk::new("y", Style::NONE),
1380        ]);
1381        eprintln!("[TEST] Buffer text: {:?}", buffer.to_string());
1382
1383        // Set tab indicator
1384        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        // Tab indicator at position 1
1392        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        // Background is preserved from the style
1404        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        // Default tab width is 4
1426        // "ab" at positions 0,1
1427        // TAB at positions 2,3 (expands to fill to next multiple of 4)
1428        // "cd" at positions 4,5
1429        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        // Tab expansion fills 2,3
1440        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        // After tab
1455        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        // Create text with a tab: "ab\tcd"
1477        // With tab width 4, "ab" at 0-1, tab expands to 2-3, "cd" at 4-5
1478        let buffer = TextBuffer::with_text("ab\tcd");
1479        let selection_bg = Rgba::rgb(0.0, 0.0, 1.0); // Blue selection
1480        let selection_style = Style::NONE.with_bg(selection_bg);
1481
1482        let mut view = TextBufferView::new(&buffer).viewport(0, 0, 80, 24);
1483
1484        // Select just the tab character (character offset 2)
1485        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        // Tab expands to positions 2 and 3 (fill to next multiple of 4)
1493        // Both should have the selection background
1494        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        // Characters before and after tab should NOT have selection
1513        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    // ================== LineInfo Comprehensive Tests ==================
1529
1530    #[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        // "Hello " (6) + "world" would exceed 10, so wrap at "Hello "
1649        // Then "world " (6) + "test" (4) = 10, fits
1650        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        // Long word without spaces should still break at char boundaries
1683        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        // "This is longer" should wrap
1715        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        // CJK characters are typically 2 columns wide
1780        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        // Verify no line has an odd-width ending that would split a CJK char
1815        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        // ZWJ family emoji (👨‍👩‍👧) is multiple codepoints but displays as width 2
1828        // Each emoji: 👨 (U+1F468) + ZWJ (U+200D) + 👩 (U+1F469) + ZWJ + 👧 (U+1F467)
1829        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        // The family emoji should render as width 2
1841        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        // Text: "AB" + family emoji + "CD" = 2 + 2 + 2 = 6 display columns
1853        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        // With width 3:
1868        // Line 0: "AB" (2 cols) - emoji doesn't fit, wraps
1869        // Line 1: family emoji (2 cols) - fits
1870        // Line 2: "CD" (2 cols)
1871        // Verify no line exceeds wrap width
1872        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        // Create new buffer with different content
1897        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        // Test source_to_virtual
1968        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        // Test virtual_to_source
1975        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        // Test round-trip: source -> virtual -> source
1982        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        // Verify out-of-bounds returns None
2012        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        // Generate 10K lines of text
2061        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        // Allow up to 150ms for CI/slow/loaded machines, but log if over 10ms
2091        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            // Confirm it's NOT a placeholder (pool_id > 0)
2118            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            // Verify content is preserved in pool
2125            assert_eq!(pool.get(id), Some("👨‍👩‍👧"));
2126        } else {
2127            assert!(
2128                matches!(cell.content, CellContent::Grapheme(_)),
2129                "Expected Grapheme content"
2130            );
2131        }
2132    }
2133}