use crate::intervals::StyleId;
use crate::layout::{
DEFAULT_TAB_WIDTH, LayoutEngine, WrapIndent, WrapMode, cell_width_at, visual_x_for_column,
wrap_indent_cells_for_line_text,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub ch: char,
pub width: usize,
pub styles: Vec<StyleId>,
}
impl Cell {
pub fn new(ch: char, width: usize) -> Self {
Self {
ch,
width,
styles: Vec::new(),
}
}
pub fn with_styles(ch: char, width: usize, styles: Vec<StyleId>) -> Self {
Self { ch, width, styles }
}
}
#[derive(Debug, Clone)]
pub struct HeadlessLine {
pub logical_line_index: usize,
pub is_wrapped_part: bool,
pub visual_in_logical: usize,
pub char_offset_start: usize,
pub char_offset_end: usize,
pub segment_x_start_cells: usize,
pub is_fold_placeholder_appended: bool,
pub cells: Vec<Cell>,
}
impl HeadlessLine {
pub fn new(logical_line_index: usize, is_wrapped_part: bool) -> Self {
Self {
logical_line_index,
is_wrapped_part,
visual_in_logical: if is_wrapped_part { 1 } else { 0 },
char_offset_start: 0,
char_offset_end: 0,
segment_x_start_cells: 0,
is_fold_placeholder_appended: false,
cells: Vec::new(),
}
}
pub fn set_visual_metadata(
&mut self,
visual_in_logical: usize,
char_offset_start: usize,
char_offset_end: usize,
segment_x_start_cells: usize,
) {
self.visual_in_logical = visual_in_logical;
self.char_offset_start = char_offset_start;
self.char_offset_end = char_offset_end;
self.segment_x_start_cells = segment_x_start_cells;
}
pub fn set_fold_placeholder_appended(&mut self, appended: bool) {
self.is_fold_placeholder_appended = appended;
}
pub fn add_cell(&mut self, cell: Cell) {
self.cells.push(cell);
}
pub fn visual_width(&self) -> usize {
self.cells.iter().map(|c| c.width).sum()
}
}
#[derive(Debug, Clone)]
pub struct HeadlessGrid {
pub lines: Vec<HeadlessLine>,
pub start_visual_row: usize,
pub count: usize,
}
impl HeadlessGrid {
pub fn new(start_visual_row: usize, count: usize) -> Self {
Self {
lines: Vec::new(),
start_visual_row,
count,
}
}
pub fn add_line(&mut self, line: HeadlessLine) {
self.lines.push(line);
}
pub fn actual_line_count(&self) -> usize {
self.lines.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MinimapLine {
pub logical_line_index: usize,
pub visual_in_logical: usize,
pub char_offset_start: usize,
pub char_offset_end: usize,
pub total_cells: usize,
pub non_whitespace_cells: usize,
pub dominant_style: Option<StyleId>,
pub is_fold_placeholder_appended: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MinimapGrid {
pub lines: Vec<MinimapLine>,
pub start_visual_row: usize,
pub count: usize,
}
impl MinimapGrid {
pub fn new(start_visual_row: usize, count: usize) -> Self {
Self {
lines: Vec::new(),
start_visual_row,
count,
}
}
pub fn actual_line_count(&self) -> usize {
self.lines.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComposedCell {
pub ch: char,
pub width: usize,
pub styles: Vec<crate::intervals::StyleId>,
pub source: ComposedCellSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposedCellSource {
Document {
offset: usize,
},
Virtual {
anchor_offset: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposedLineKind {
Document {
logical_line: usize,
visual_in_logical: usize,
},
VirtualAboveLine {
logical_line: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComposedLine {
pub kind: ComposedLineKind,
pub char_offset_start: usize,
pub char_offset_end: usize,
pub cells: Vec<ComposedCell>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComposedGrid {
pub lines: Vec<ComposedLine>,
pub start_visual_row: usize,
pub count: usize,
}
impl ComposedGrid {
pub fn new(start_visual_row: usize, count: usize) -> Self {
Self {
lines: Vec::new(),
start_visual_row,
count,
}
}
pub fn actual_line_count(&self) -> usize {
self.lines.len()
}
}
pub struct SnapshotGenerator {
lines: Vec<String>,
viewport_width: usize,
tab_width: usize,
layout_engine: LayoutEngine,
}
impl SnapshotGenerator {
pub fn new(viewport_width: usize) -> Self {
let lines = vec![String::new()];
let mut layout_engine = LayoutEngine::new(viewport_width);
let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
layout_engine.from_lines(&line_refs);
Self {
lines,
viewport_width,
tab_width: layout_engine.tab_width(),
layout_engine,
}
}
pub fn from_text(text: &str, viewport_width: usize) -> Self {
Self::from_text_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
}
pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
Self::from_text_with_options(text, viewport_width, tab_width, WrapMode::Char)
}
pub fn from_text_with_options(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
) -> Self {
Self::from_text_with_layout_options(
text,
viewport_width,
tab_width,
wrap_mode,
WrapIndent::None,
)
}
pub fn from_text_with_layout_options(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
wrap_indent: WrapIndent,
) -> Self {
let normalized = crate::text::normalize_crlf_to_lf(text);
let lines = crate::text::split_lines_preserve_trailing(normalized.as_ref());
let mut layout_engine = LayoutEngine::new(viewport_width);
layout_engine.set_tab_width(tab_width);
layout_engine.set_wrap_mode(wrap_mode);
layout_engine.set_wrap_indent(wrap_indent);
let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
layout_engine.from_lines(&line_refs);
Self {
lines,
viewport_width,
tab_width: layout_engine.tab_width(),
layout_engine,
}
}
pub fn set_lines(&mut self, lines: Vec<String>) {
self.lines = if lines.is_empty() {
vec![String::new()]
} else {
lines
};
self.reflow_layout();
}
pub fn set_viewport_width(&mut self, width: usize) {
self.viewport_width = width;
self.layout_engine.set_viewport_width(width);
self.reflow_layout();
}
pub fn set_tab_width(&mut self, tab_width: usize) {
self.tab_width = tab_width.max(1);
self.layout_engine.set_tab_width(self.tab_width);
self.reflow_layout();
}
pub fn tab_width(&self) -> usize {
self.tab_width
}
fn reflow_layout(&mut self) {
self.layout_engine
.recalculate_all_from_lines(self.lines.iter().map(String::as_str));
}
pub fn get_headless_grid(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
let mut grid = HeadlessGrid::new(start_visual_row, count);
if count == 0 {
return grid;
}
let total_visual = self.layout_engine.visual_line_count();
if start_visual_row >= total_visual {
return grid;
}
let end_visual = start_visual_row.saturating_add(count).min(total_visual);
let mut current_visual = 0usize;
let mut line_start_offset = 0usize;
for logical_line in 0..self.layout_engine.logical_line_count() {
let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
continue;
};
let line_text = self
.lines
.get(logical_line)
.map(|s| s.as_str())
.unwrap_or("");
let line_char_len = line_text.chars().count();
for visual_in_line in 0..layout.visual_line_count {
if current_visual >= end_visual {
return grid;
}
if current_visual >= start_visual_row {
let segment_start_col = if visual_in_line == 0 {
0
} else {
layout
.wrap_points
.get(visual_in_line - 1)
.map(|wp| wp.char_index)
.unwrap_or(0)
.min(line_char_len)
};
let segment_end_col = if visual_in_line < layout.wrap_points.len() {
layout.wrap_points[visual_in_line]
.char_index
.min(line_char_len)
} else {
line_char_len
};
let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
let mut segment_x_start_cells = 0usize;
if visual_in_line > 0 {
let indent_cells = wrap_indent_cells_for_line_text(
line_text,
self.layout_engine.wrap_indent(),
self.viewport_width,
self.tab_width,
);
segment_x_start_cells = indent_cells;
for _ in 0..indent_cells {
headless_line.add_cell(Cell::new(' ', 1));
}
}
let seg_start_x_in_line =
visual_x_for_column(line_text, segment_start_col, self.tab_width);
let mut x_in_line = seg_start_x_in_line;
for ch in line_text
.chars()
.skip(segment_start_col)
.take(segment_end_col.saturating_sub(segment_start_col))
{
let w = cell_width_at(ch, x_in_line, self.tab_width);
x_in_line = x_in_line.saturating_add(w);
headless_line.add_cell(Cell::new(ch, w));
}
headless_line.set_visual_metadata(
visual_in_line,
line_start_offset.saturating_add(segment_start_col),
line_start_offset.saturating_add(segment_end_col),
segment_x_start_cells,
);
grid.add_line(headless_line);
}
current_visual = current_visual.saturating_add(1);
}
line_start_offset = line_start_offset.saturating_add(line_char_len);
if logical_line + 1 < self.layout_engine.logical_line_count() {
line_start_offset = line_start_offset.saturating_add(1);
}
}
grid
}
pub fn get_line(&self, line_index: usize) -> Option<&str> {
self.lines.get(line_index).map(|s| s.as_str())
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_creation() {
let cell = Cell::new('a', 1);
assert_eq!(cell.ch, 'a');
assert_eq!(cell.width, 1);
assert!(cell.styles.is_empty());
}
#[test]
fn test_cell_with_styles() {
let cell = Cell::with_styles('你', 2, vec![1, 2, 3]);
assert_eq!(cell.ch, '你');
assert_eq!(cell.width, 2);
assert_eq!(cell.styles, vec![1, 2, 3]);
}
#[test]
fn test_headless_line() {
let mut line = HeadlessLine::new(0, false);
line.add_cell(Cell::new('H', 1));
line.add_cell(Cell::new('e', 1));
line.add_cell(Cell::new('你', 2));
assert_eq!(line.logical_line_index, 0);
assert!(!line.is_wrapped_part);
assert_eq!(line.visual_in_logical, 0);
assert_eq!(line.char_offset_start, 0);
assert_eq!(line.char_offset_end, 0);
assert_eq!(line.segment_x_start_cells, 0);
assert!(!line.is_fold_placeholder_appended);
assert_eq!(line.cells.len(), 3);
assert_eq!(line.visual_width(), 4); }
#[test]
fn test_snapshot_generator_basic() {
let text = "Hello\nWorld\nRust";
let generator = SnapshotGenerator::from_text(text, 80);
assert_eq!(generator.line_count(), 3);
assert_eq!(generator.get_line(0), Some("Hello"));
assert_eq!(generator.get_line(1), Some("World"));
assert_eq!(generator.get_line(2), Some("Rust"));
}
#[test]
fn test_get_headless_grid() {
let text = "Line 1\nLine 2\nLine 3\nLine 4";
let generator = SnapshotGenerator::from_text(text, 80);
let grid = generator.get_headless_grid(0, 2);
assert_eq!(grid.start_visual_row, 0);
assert_eq!(grid.count, 2);
assert_eq!(grid.actual_line_count(), 2);
let line0 = &grid.lines[0];
assert_eq!(line0.logical_line_index, 0);
assert!(!line0.is_wrapped_part);
assert_eq!(line0.visual_in_logical, 0);
assert_eq!(line0.char_offset_start, 0);
assert_eq!(line0.char_offset_end, 6);
assert_eq!(line0.cells.len(), 6);
let grid2 = generator.get_headless_grid(1, 2);
assert_eq!(grid2.actual_line_count(), 2);
assert_eq!(grid2.lines[0].logical_line_index, 1);
assert_eq!(grid2.lines[1].logical_line_index, 2);
}
#[test]
fn test_get_headless_grid_soft_wrap_single_line() {
let generator = SnapshotGenerator::from_text("abcd", 2);
let grid = generator.get_headless_grid(0, 10);
assert_eq!(grid.actual_line_count(), 2);
let line0_text: String = grid.lines[0].cells.iter().map(|c| c.ch).collect();
let line1_text: String = grid.lines[1].cells.iter().map(|c| c.ch).collect();
assert_eq!(grid.lines[0].logical_line_index, 0);
assert!(!grid.lines[0].is_wrapped_part);
assert_eq!(grid.lines[0].visual_in_logical, 0);
assert_eq!(line0_text, "ab");
assert_eq!(grid.lines[1].logical_line_index, 0);
assert!(grid.lines[1].is_wrapped_part);
assert_eq!(grid.lines[1].visual_in_logical, 1);
assert_eq!(line1_text, "cd");
let grid2 = generator.get_headless_grid(1, 1);
assert_eq!(grid2.actual_line_count(), 1);
assert_eq!(grid2.lines[0].logical_line_index, 0);
assert!(grid2.lines[0].is_wrapped_part);
let text2: String = grid2.lines[0].cells.iter().map(|c| c.ch).collect();
assert_eq!(text2, "cd");
}
#[test]
fn test_grid_with_cjk() {
let text = "Hello\n你好世界\nRust";
let generator = SnapshotGenerator::from_text(text, 80);
let grid = generator.get_headless_grid(1, 1);
let line = &grid.lines[0];
assert_eq!(line.cells.len(), 4); assert_eq!(line.visual_width(), 8);
assert_eq!(line.cells[0].ch, '你');
assert_eq!(line.cells[0].width, 2);
assert_eq!(line.cells[1].ch, '好');
assert_eq!(line.cells[1].width, 2);
}
#[test]
fn test_grid_with_emoji() {
let text = "Hello 👋\nWorld 🌍";
let generator = SnapshotGenerator::from_text(text, 80);
let grid = generator.get_headless_grid(0, 2);
assert_eq!(grid.actual_line_count(), 2);
let line0 = &grid.lines[0];
assert_eq!(line0.cells.len(), 7); assert_eq!(line0.visual_width(), 8);
}
#[test]
fn test_grid_bounds() {
let text = "Line 1\nLine 2\nLine 3";
let generator = SnapshotGenerator::from_text(text, 80);
let grid = generator.get_headless_grid(1, 10);
assert_eq!(grid.actual_line_count(), 2);
let grid2 = generator.get_headless_grid(10, 5);
assert_eq!(grid2.actual_line_count(), 0);
}
#[test]
fn test_empty_document() {
let generator = SnapshotGenerator::new(80);
let grid = generator.get_headless_grid(0, 10);
assert_eq!(grid.actual_line_count(), 1);
}
#[test]
fn test_viewport_width_change() {
let text = "Hello World";
let mut generator = SnapshotGenerator::from_text(text, 40);
assert_eq!(generator.viewport_width, 40);
generator.set_viewport_width(20);
assert_eq!(generator.viewport_width, 20);
generator.set_viewport_width(5);
let grid = generator.get_headless_grid(0, 10);
assert!(grid.actual_line_count() > 1);
}
}