1use crate::intervals::StyleId;
6use crate::layout::{LayoutEngine, char_width};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Cell {
11 pub ch: char,
13 pub width: usize,
15 pub styles: Vec<StyleId>,
17}
18
19impl Cell {
20 pub fn new(ch: char, width: usize) -> Self {
22 Self {
23 ch,
24 width,
25 styles: Vec::new(),
26 }
27 }
28
29 pub fn with_styles(ch: char, width: usize, styles: Vec<StyleId>) -> Self {
31 Self { ch, width, styles }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct HeadlessLine {
38 pub logical_line_index: usize,
40 pub is_wrapped_part: bool,
42 pub cells: Vec<Cell>,
44}
45
46impl HeadlessLine {
47 pub fn new(logical_line_index: usize, is_wrapped_part: bool) -> Self {
49 Self {
50 logical_line_index,
51 is_wrapped_part,
52 cells: Vec::new(),
53 }
54 }
55
56 pub fn add_cell(&mut self, cell: Cell) {
58 self.cells.push(cell);
59 }
60
61 pub fn visual_width(&self) -> usize {
63 self.cells.iter().map(|c| c.width).sum()
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct HeadlessGrid {
70 pub lines: Vec<HeadlessLine>,
72 pub start_visual_row: usize,
74 pub count: usize,
76}
77
78impl HeadlessGrid {
79 pub fn new(start_visual_row: usize, count: usize) -> Self {
81 Self {
82 lines: Vec::new(),
83 start_visual_row,
84 count,
85 }
86 }
87
88 pub fn add_line(&mut self, line: HeadlessLine) {
90 self.lines.push(line);
91 }
92
93 pub fn actual_line_count(&self) -> usize {
95 self.lines.len()
96 }
97}
98
99pub struct SnapshotGenerator {
103 lines: Vec<String>,
105 viewport_width: usize,
107 layout_engine: LayoutEngine,
109}
110
111impl SnapshotGenerator {
112 pub fn new(viewport_width: usize) -> Self {
114 let lines = vec![String::new()];
115 let mut layout_engine = LayoutEngine::new(viewport_width);
116 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
117 layout_engine.from_lines(&line_refs);
118
119 Self {
120 lines,
122 viewport_width,
123 layout_engine,
124 }
125 }
126
127 pub fn from_text(text: &str, viewport_width: usize) -> Self {
129 let lines = crate::text::split_lines_preserve_trailing(text);
130 let mut layout_engine = LayoutEngine::new(viewport_width);
131 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
132 layout_engine.from_lines(&line_refs);
133 Self {
134 lines,
135 viewport_width,
136 layout_engine,
137 }
138 }
139
140 pub fn set_lines(&mut self, lines: Vec<String>) {
142 self.lines = if lines.is_empty() {
143 vec![String::new()]
144 } else {
145 lines
146 };
147
148 let line_refs: Vec<&str> = self.lines.iter().map(|s| s.as_str()).collect();
149 self.layout_engine.from_lines(&line_refs);
150 }
151
152 pub fn set_viewport_width(&mut self, width: usize) {
154 self.viewport_width = width;
155 self.layout_engine.set_viewport_width(width);
156 }
157
158 pub fn get_headless_grid(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
162 let mut grid = HeadlessGrid::new(start_visual_row, count);
163
164 if count == 0 {
165 return grid;
166 }
167
168 let total_visual = self.layout_engine.visual_line_count();
169 if start_visual_row >= total_visual {
170 return grid;
171 }
172
173 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
174 let mut current_visual = 0usize;
175
176 for logical_line in 0..self.layout_engine.logical_line_count() {
177 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
178 continue;
179 };
180
181 let line_text = self
182 .lines
183 .get(logical_line)
184 .map(|s| s.as_str())
185 .unwrap_or("");
186 let line_char_len = line_text.chars().count();
187
188 for visual_in_line in 0..layout.visual_line_count {
189 if current_visual >= end_visual {
190 return grid;
191 }
192
193 if current_visual >= start_visual_row {
194 let segment_start_col = if visual_in_line == 0 {
195 0
196 } else {
197 layout
198 .wrap_points
199 .get(visual_in_line - 1)
200 .map(|wp| wp.char_index)
201 .unwrap_or(0)
202 .min(line_char_len)
203 };
204
205 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
206 layout.wrap_points[visual_in_line]
207 .char_index
208 .min(line_char_len)
209 } else {
210 line_char_len
211 };
212
213 let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
214 for ch in line_text
215 .chars()
216 .skip(segment_start_col)
217 .take(segment_end_col.saturating_sub(segment_start_col))
218 {
219 headless_line.add_cell(Cell::new(ch, char_width(ch)));
220 }
221
222 grid.add_line(headless_line);
223 }
224
225 current_visual = current_visual.saturating_add(1);
226 }
227 }
228
229 grid
230 }
231
232 pub fn get_line(&self, line_index: usize) -> Option<&str> {
234 self.lines.get(line_index).map(|s| s.as_str())
235 }
236
237 pub fn line_count(&self) -> usize {
239 self.lines.len()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_cell_creation() {
249 let cell = Cell::new('a', 1);
250 assert_eq!(cell.ch, 'a');
251 assert_eq!(cell.width, 1);
252 assert!(cell.styles.is_empty());
253 }
254
255 #[test]
256 fn test_cell_with_styles() {
257 let cell = Cell::with_styles('你', 2, vec![1, 2, 3]);
258 assert_eq!(cell.ch, '你');
259 assert_eq!(cell.width, 2);
260 assert_eq!(cell.styles, vec![1, 2, 3]);
261 }
262
263 #[test]
264 fn test_headless_line() {
265 let mut line = HeadlessLine::new(0, false);
266 line.add_cell(Cell::new('H', 1));
267 line.add_cell(Cell::new('e', 1));
268 line.add_cell(Cell::new('你', 2));
269
270 assert_eq!(line.logical_line_index, 0);
271 assert!(!line.is_wrapped_part);
272 assert_eq!(line.cells.len(), 3);
273 assert_eq!(line.visual_width(), 4); }
275
276 #[test]
277 fn test_snapshot_generator_basic() {
278 let text = "Hello\nWorld\nRust";
279 let generator = SnapshotGenerator::from_text(text, 80);
280
281 assert_eq!(generator.line_count(), 3);
282 assert_eq!(generator.get_line(0), Some("Hello"));
283 assert_eq!(generator.get_line(1), Some("World"));
284 assert_eq!(generator.get_line(2), Some("Rust"));
285 }
286
287 #[test]
288 fn test_get_headless_grid() {
289 let text = "Line 1\nLine 2\nLine 3\nLine 4";
290 let generator = SnapshotGenerator::from_text(text, 80);
291
292 let grid = generator.get_headless_grid(0, 2);
294 assert_eq!(grid.start_visual_row, 0);
295 assert_eq!(grid.count, 2);
296 assert_eq!(grid.actual_line_count(), 2);
297
298 let line0 = &grid.lines[0];
300 assert_eq!(line0.logical_line_index, 0);
301 assert!(!line0.is_wrapped_part);
302 assert_eq!(line0.cells.len(), 6); let grid2 = generator.get_headless_grid(1, 2);
306 assert_eq!(grid2.actual_line_count(), 2);
307 assert_eq!(grid2.lines[0].logical_line_index, 1);
308 assert_eq!(grid2.lines[1].logical_line_index, 2);
309 }
310
311 #[test]
312 fn test_get_headless_grid_soft_wrap_single_line() {
313 let generator = SnapshotGenerator::from_text("abcd", 2);
314
315 let grid = generator.get_headless_grid(0, 10);
316 assert_eq!(grid.actual_line_count(), 2);
317
318 let line0_text: String = grid.lines[0].cells.iter().map(|c| c.ch).collect();
319 let line1_text: String = grid.lines[1].cells.iter().map(|c| c.ch).collect();
320
321 assert_eq!(grid.lines[0].logical_line_index, 0);
322 assert!(!grid.lines[0].is_wrapped_part);
323 assert_eq!(line0_text, "ab");
324
325 assert_eq!(grid.lines[1].logical_line_index, 0);
326 assert!(grid.lines[1].is_wrapped_part);
327 assert_eq!(line1_text, "cd");
328
329 let grid2 = generator.get_headless_grid(1, 1);
331 assert_eq!(grid2.actual_line_count(), 1);
332 assert_eq!(grid2.lines[0].logical_line_index, 0);
333 assert!(grid2.lines[0].is_wrapped_part);
334 let text2: String = grid2.lines[0].cells.iter().map(|c| c.ch).collect();
335 assert_eq!(text2, "cd");
336 }
337
338 #[test]
339 fn test_grid_with_cjk() {
340 let text = "Hello\n你好世界\nRust";
341 let generator = SnapshotGenerator::from_text(text, 80);
342
343 let grid = generator.get_headless_grid(1, 1);
344 let line = &grid.lines[0];
345
346 assert_eq!(line.cells.len(), 4); assert_eq!(line.visual_width(), 8); assert_eq!(line.cells[0].ch, '你');
351 assert_eq!(line.cells[0].width, 2);
352 assert_eq!(line.cells[1].ch, '好');
353 assert_eq!(line.cells[1].width, 2);
354 }
355
356 #[test]
357 fn test_grid_with_emoji() {
358 let text = "Hello 👋\nWorld 🌍";
359 let generator = SnapshotGenerator::from_text(text, 80);
360
361 let grid = generator.get_headless_grid(0, 2);
362 assert_eq!(grid.actual_line_count(), 2);
363
364 let line0 = &grid.lines[0];
366 assert_eq!(line0.cells.len(), 7); assert_eq!(line0.visual_width(), 8);
369 }
370
371 #[test]
372 fn test_grid_bounds() {
373 let text = "Line 1\nLine 2\nLine 3";
374 let generator = SnapshotGenerator::from_text(text, 80);
375
376 let grid = generator.get_headless_grid(1, 10);
378 assert_eq!(grid.actual_line_count(), 2); let grid2 = generator.get_headless_grid(10, 5);
383 assert_eq!(grid2.actual_line_count(), 0);
384 }
385
386 #[test]
387 fn test_empty_document() {
388 let generator = SnapshotGenerator::new(80);
389 let grid = generator.get_headless_grid(0, 10);
390 assert_eq!(grid.actual_line_count(), 1);
391 }
392
393 #[test]
394 fn test_viewport_width_change() {
395 let text = "Hello World";
396 let mut generator = SnapshotGenerator::from_text(text, 40);
397
398 assert_eq!(generator.viewport_width, 40);
399
400 generator.set_viewport_width(20);
401 assert_eq!(generator.viewport_width, 20);
402 generator.set_viewport_width(5);
404 let grid = generator.get_headless_grid(0, 10);
405 assert!(grid.actual_line_count() > 1);
406 }
407}