1use crate::intervals::StyleId;
6use crate::layout::{
7 DEFAULT_TAB_WIDTH, LayoutEngine, WrapIndent, WrapMode, cell_width_at, visual_x_for_column,
8 wrap_indent_cells_for_line_text,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Cell {
14 pub ch: char,
16 pub width: usize,
18 pub styles: Vec<StyleId>,
20}
21
22impl Cell {
23 pub fn new(ch: char, width: usize) -> Self {
25 Self {
26 ch,
27 width,
28 styles: Vec::new(),
29 }
30 }
31
32 pub fn with_styles(ch: char, width: usize, styles: Vec<StyleId>) -> Self {
34 Self { ch, width, styles }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct HeadlessLine {
41 pub logical_line_index: usize,
43 pub is_wrapped_part: bool,
45 pub cells: Vec<Cell>,
47}
48
49impl HeadlessLine {
50 pub fn new(logical_line_index: usize, is_wrapped_part: bool) -> Self {
52 Self {
53 logical_line_index,
54 is_wrapped_part,
55 cells: Vec::new(),
56 }
57 }
58
59 pub fn add_cell(&mut self, cell: Cell) {
61 self.cells.push(cell);
62 }
63
64 pub fn visual_width(&self) -> usize {
66 self.cells.iter().map(|c| c.width).sum()
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct HeadlessGrid {
73 pub lines: Vec<HeadlessLine>,
75 pub start_visual_row: usize,
77 pub count: usize,
79}
80
81impl HeadlessGrid {
82 pub fn new(start_visual_row: usize, count: usize) -> Self {
84 Self {
85 lines: Vec::new(),
86 start_visual_row,
87 count,
88 }
89 }
90
91 pub fn add_line(&mut self, line: HeadlessLine) {
93 self.lines.push(line);
94 }
95
96 pub fn actual_line_count(&self) -> usize {
98 self.lines.len()
99 }
100}
101
102pub struct SnapshotGenerator {
106 lines: Vec<String>,
108 viewport_width: usize,
110 tab_width: usize,
112 layout_engine: LayoutEngine,
114}
115
116impl SnapshotGenerator {
117 pub fn new(viewport_width: usize) -> Self {
119 let lines = vec![String::new()];
120 let mut layout_engine = LayoutEngine::new(viewport_width);
121 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
122 layout_engine.from_lines(&line_refs);
123
124 Self {
125 lines,
127 viewport_width,
128 tab_width: layout_engine.tab_width(),
129 layout_engine,
130 }
131 }
132
133 pub fn from_text(text: &str, viewport_width: usize) -> Self {
135 Self::from_text_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
136 }
137
138 pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
140 Self::from_text_with_options(text, viewport_width, tab_width, WrapMode::Char)
141 }
142
143 pub fn from_text_with_options(
145 text: &str,
146 viewport_width: usize,
147 tab_width: usize,
148 wrap_mode: WrapMode,
149 ) -> Self {
150 Self::from_text_with_layout_options(
151 text,
152 viewport_width,
153 tab_width,
154 wrap_mode,
155 WrapIndent::None,
156 )
157 }
158
159 pub fn from_text_with_layout_options(
161 text: &str,
162 viewport_width: usize,
163 tab_width: usize,
164 wrap_mode: WrapMode,
165 wrap_indent: WrapIndent,
166 ) -> Self {
167 let normalized = crate::text::normalize_crlf_to_lf(text);
168 let lines = crate::text::split_lines_preserve_trailing(normalized.as_ref());
169 let mut layout_engine = LayoutEngine::new(viewport_width);
170 layout_engine.set_tab_width(tab_width);
171 layout_engine.set_wrap_mode(wrap_mode);
172 layout_engine.set_wrap_indent(wrap_indent);
173 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
174 layout_engine.from_lines(&line_refs);
175 Self {
176 lines,
177 viewport_width,
178 tab_width: layout_engine.tab_width(),
179 layout_engine,
180 }
181 }
182
183 pub fn set_lines(&mut self, lines: Vec<String>) {
185 self.lines = if lines.is_empty() {
186 vec![String::new()]
187 } else {
188 lines
189 };
190
191 let line_refs: Vec<&str> = self.lines.iter().map(|s| s.as_str()).collect();
192 self.layout_engine.from_lines(&line_refs);
193 }
194
195 pub fn set_viewport_width(&mut self, width: usize) {
197 self.viewport_width = width;
198 self.layout_engine.set_viewport_width(width);
199 }
200
201 pub fn set_tab_width(&mut self, tab_width: usize) {
203 self.tab_width = tab_width.max(1);
204 self.layout_engine.set_tab_width(self.tab_width);
205 }
206
207 pub fn tab_width(&self) -> usize {
209 self.tab_width
210 }
211
212 pub fn get_headless_grid(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
216 let mut grid = HeadlessGrid::new(start_visual_row, count);
217
218 if count == 0 {
219 return grid;
220 }
221
222 let total_visual = self.layout_engine.visual_line_count();
223 if start_visual_row >= total_visual {
224 return grid;
225 }
226
227 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
228 let mut current_visual = 0usize;
229
230 for logical_line in 0..self.layout_engine.logical_line_count() {
231 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
232 continue;
233 };
234
235 let line_text = self
236 .lines
237 .get(logical_line)
238 .map(|s| s.as_str())
239 .unwrap_or("");
240 let line_char_len = line_text.chars().count();
241
242 for visual_in_line in 0..layout.visual_line_count {
243 if current_visual >= end_visual {
244 return grid;
245 }
246
247 if current_visual >= start_visual_row {
248 let segment_start_col = if visual_in_line == 0 {
249 0
250 } else {
251 layout
252 .wrap_points
253 .get(visual_in_line - 1)
254 .map(|wp| wp.char_index)
255 .unwrap_or(0)
256 .min(line_char_len)
257 };
258
259 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
260 layout.wrap_points[visual_in_line]
261 .char_index
262 .min(line_char_len)
263 } else {
264 line_char_len
265 };
266
267 let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
268 if visual_in_line > 0 {
269 let indent_cells = wrap_indent_cells_for_line_text(
270 line_text,
271 self.layout_engine.wrap_indent(),
272 self.viewport_width,
273 self.tab_width,
274 );
275 for _ in 0..indent_cells {
276 headless_line.add_cell(Cell::new(' ', 1));
277 }
278 }
279 let seg_start_x_in_line =
280 visual_x_for_column(line_text, segment_start_col, self.tab_width);
281 let mut x_in_line = seg_start_x_in_line;
282 for ch in line_text
283 .chars()
284 .skip(segment_start_col)
285 .take(segment_end_col.saturating_sub(segment_start_col))
286 {
287 let w = cell_width_at(ch, x_in_line, self.tab_width);
288 x_in_line = x_in_line.saturating_add(w);
289 headless_line.add_cell(Cell::new(ch, w));
290 }
291
292 grid.add_line(headless_line);
293 }
294
295 current_visual = current_visual.saturating_add(1);
296 }
297 }
298
299 grid
300 }
301
302 pub fn get_line(&self, line_index: usize) -> Option<&str> {
304 self.lines.get(line_index).map(|s| s.as_str())
305 }
306
307 pub fn line_count(&self) -> usize {
309 self.lines.len()
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_cell_creation() {
319 let cell = Cell::new('a', 1);
320 assert_eq!(cell.ch, 'a');
321 assert_eq!(cell.width, 1);
322 assert!(cell.styles.is_empty());
323 }
324
325 #[test]
326 fn test_cell_with_styles() {
327 let cell = Cell::with_styles('你', 2, vec![1, 2, 3]);
328 assert_eq!(cell.ch, '你');
329 assert_eq!(cell.width, 2);
330 assert_eq!(cell.styles, vec![1, 2, 3]);
331 }
332
333 #[test]
334 fn test_headless_line() {
335 let mut line = HeadlessLine::new(0, false);
336 line.add_cell(Cell::new('H', 1));
337 line.add_cell(Cell::new('e', 1));
338 line.add_cell(Cell::new('你', 2));
339
340 assert_eq!(line.logical_line_index, 0);
341 assert!(!line.is_wrapped_part);
342 assert_eq!(line.cells.len(), 3);
343 assert_eq!(line.visual_width(), 4); }
345
346 #[test]
347 fn test_snapshot_generator_basic() {
348 let text = "Hello\nWorld\nRust";
349 let generator = SnapshotGenerator::from_text(text, 80);
350
351 assert_eq!(generator.line_count(), 3);
352 assert_eq!(generator.get_line(0), Some("Hello"));
353 assert_eq!(generator.get_line(1), Some("World"));
354 assert_eq!(generator.get_line(2), Some("Rust"));
355 }
356
357 #[test]
358 fn test_get_headless_grid() {
359 let text = "Line 1\nLine 2\nLine 3\nLine 4";
360 let generator = SnapshotGenerator::from_text(text, 80);
361
362 let grid = generator.get_headless_grid(0, 2);
364 assert_eq!(grid.start_visual_row, 0);
365 assert_eq!(grid.count, 2);
366 assert_eq!(grid.actual_line_count(), 2);
367
368 let line0 = &grid.lines[0];
370 assert_eq!(line0.logical_line_index, 0);
371 assert!(!line0.is_wrapped_part);
372 assert_eq!(line0.cells.len(), 6); let grid2 = generator.get_headless_grid(1, 2);
376 assert_eq!(grid2.actual_line_count(), 2);
377 assert_eq!(grid2.lines[0].logical_line_index, 1);
378 assert_eq!(grid2.lines[1].logical_line_index, 2);
379 }
380
381 #[test]
382 fn test_get_headless_grid_soft_wrap_single_line() {
383 let generator = SnapshotGenerator::from_text("abcd", 2);
384
385 let grid = generator.get_headless_grid(0, 10);
386 assert_eq!(grid.actual_line_count(), 2);
387
388 let line0_text: String = grid.lines[0].cells.iter().map(|c| c.ch).collect();
389 let line1_text: String = grid.lines[1].cells.iter().map(|c| c.ch).collect();
390
391 assert_eq!(grid.lines[0].logical_line_index, 0);
392 assert!(!grid.lines[0].is_wrapped_part);
393 assert_eq!(line0_text, "ab");
394
395 assert_eq!(grid.lines[1].logical_line_index, 0);
396 assert!(grid.lines[1].is_wrapped_part);
397 assert_eq!(line1_text, "cd");
398
399 let grid2 = generator.get_headless_grid(1, 1);
401 assert_eq!(grid2.actual_line_count(), 1);
402 assert_eq!(grid2.lines[0].logical_line_index, 0);
403 assert!(grid2.lines[0].is_wrapped_part);
404 let text2: String = grid2.lines[0].cells.iter().map(|c| c.ch).collect();
405 assert_eq!(text2, "cd");
406 }
407
408 #[test]
409 fn test_grid_with_cjk() {
410 let text = "Hello\n你好世界\nRust";
411 let generator = SnapshotGenerator::from_text(text, 80);
412
413 let grid = generator.get_headless_grid(1, 1);
414 let line = &grid.lines[0];
415
416 assert_eq!(line.cells.len(), 4); assert_eq!(line.visual_width(), 8); assert_eq!(line.cells[0].ch, '你');
421 assert_eq!(line.cells[0].width, 2);
422 assert_eq!(line.cells[1].ch, '好');
423 assert_eq!(line.cells[1].width, 2);
424 }
425
426 #[test]
427 fn test_grid_with_emoji() {
428 let text = "Hello 👋\nWorld 🌍";
429 let generator = SnapshotGenerator::from_text(text, 80);
430
431 let grid = generator.get_headless_grid(0, 2);
432 assert_eq!(grid.actual_line_count(), 2);
433
434 let line0 = &grid.lines[0];
436 assert_eq!(line0.cells.len(), 7); assert_eq!(line0.visual_width(), 8);
439 }
440
441 #[test]
442 fn test_grid_bounds() {
443 let text = "Line 1\nLine 2\nLine 3";
444 let generator = SnapshotGenerator::from_text(text, 80);
445
446 let grid = generator.get_headless_grid(1, 10);
448 assert_eq!(grid.actual_line_count(), 2); let grid2 = generator.get_headless_grid(10, 5);
453 assert_eq!(grid2.actual_line_count(), 0);
454 }
455
456 #[test]
457 fn test_empty_document() {
458 let generator = SnapshotGenerator::new(80);
459 let grid = generator.get_headless_grid(0, 10);
460 assert_eq!(grid.actual_line_count(), 1);
461 }
462
463 #[test]
464 fn test_viewport_width_change() {
465 let text = "Hello World";
466 let mut generator = SnapshotGenerator::from_text(text, 40);
467
468 assert_eq!(generator.viewport_width, 40);
469
470 generator.set_viewport_width(20);
471 assert_eq!(generator.viewport_width, 20);
472 generator.set_viewport_width(5);
474 let grid = generator.get_headless_grid(0, 10);
475 assert!(grid.actual_line_count() > 1);
476 }
477}