1use crate::intervals::StyleId;
6use crate::layout::{DEFAULT_TAB_WIDTH, LayoutEngine, cell_width_at, visual_x_for_column};
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 tab_width: usize,
109 layout_engine: LayoutEngine,
111}
112
113impl SnapshotGenerator {
114 pub fn new(viewport_width: usize) -> Self {
116 let lines = vec![String::new()];
117 let mut layout_engine = LayoutEngine::new(viewport_width);
118 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
119 layout_engine.from_lines(&line_refs);
120
121 Self {
122 lines,
124 viewport_width,
125 tab_width: layout_engine.tab_width(),
126 layout_engine,
127 }
128 }
129
130 pub fn from_text(text: &str, viewport_width: usize) -> Self {
132 Self::from_text_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
133 }
134
135 pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
137 let lines = crate::text::split_lines_preserve_trailing(text);
138 let mut layout_engine = LayoutEngine::new(viewport_width);
139 layout_engine.set_tab_width(tab_width);
140 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
141 layout_engine.from_lines(&line_refs);
142 Self {
143 lines,
144 viewport_width,
145 tab_width: layout_engine.tab_width(),
146 layout_engine,
147 }
148 }
149
150 pub fn set_lines(&mut self, lines: Vec<String>) {
152 self.lines = if lines.is_empty() {
153 vec![String::new()]
154 } else {
155 lines
156 };
157
158 let line_refs: Vec<&str> = self.lines.iter().map(|s| s.as_str()).collect();
159 self.layout_engine.from_lines(&line_refs);
160 }
161
162 pub fn set_viewport_width(&mut self, width: usize) {
164 self.viewport_width = width;
165 self.layout_engine.set_viewport_width(width);
166 }
167
168 pub fn set_tab_width(&mut self, tab_width: usize) {
170 self.tab_width = tab_width.max(1);
171 self.layout_engine.set_tab_width(self.tab_width);
172 }
173
174 pub fn tab_width(&self) -> usize {
176 self.tab_width
177 }
178
179 pub fn get_headless_grid(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
183 let mut grid = HeadlessGrid::new(start_visual_row, count);
184
185 if count == 0 {
186 return grid;
187 }
188
189 let total_visual = self.layout_engine.visual_line_count();
190 if start_visual_row >= total_visual {
191 return grid;
192 }
193
194 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
195 let mut current_visual = 0usize;
196
197 for logical_line in 0..self.layout_engine.logical_line_count() {
198 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
199 continue;
200 };
201
202 let line_text = self
203 .lines
204 .get(logical_line)
205 .map(|s| s.as_str())
206 .unwrap_or("");
207 let line_char_len = line_text.chars().count();
208
209 for visual_in_line in 0..layout.visual_line_count {
210 if current_visual >= end_visual {
211 return grid;
212 }
213
214 if current_visual >= start_visual_row {
215 let segment_start_col = if visual_in_line == 0 {
216 0
217 } else {
218 layout
219 .wrap_points
220 .get(visual_in_line - 1)
221 .map(|wp| wp.char_index)
222 .unwrap_or(0)
223 .min(line_char_len)
224 };
225
226 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
227 layout.wrap_points[visual_in_line]
228 .char_index
229 .min(line_char_len)
230 } else {
231 line_char_len
232 };
233
234 let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
235 let seg_start_x_in_line =
236 visual_x_for_column(line_text, segment_start_col, self.tab_width);
237 let mut x_in_line = seg_start_x_in_line;
238 for ch in line_text
239 .chars()
240 .skip(segment_start_col)
241 .take(segment_end_col.saturating_sub(segment_start_col))
242 {
243 let w = cell_width_at(ch, x_in_line, self.tab_width);
244 x_in_line = x_in_line.saturating_add(w);
245 headless_line.add_cell(Cell::new(ch, w));
246 }
247
248 grid.add_line(headless_line);
249 }
250
251 current_visual = current_visual.saturating_add(1);
252 }
253 }
254
255 grid
256 }
257
258 pub fn get_line(&self, line_index: usize) -> Option<&str> {
260 self.lines.get(line_index).map(|s| s.as_str())
261 }
262
263 pub fn line_count(&self) -> usize {
265 self.lines.len()
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_cell_creation() {
275 let cell = Cell::new('a', 1);
276 assert_eq!(cell.ch, 'a');
277 assert_eq!(cell.width, 1);
278 assert!(cell.styles.is_empty());
279 }
280
281 #[test]
282 fn test_cell_with_styles() {
283 let cell = Cell::with_styles('你', 2, vec![1, 2, 3]);
284 assert_eq!(cell.ch, '你');
285 assert_eq!(cell.width, 2);
286 assert_eq!(cell.styles, vec![1, 2, 3]);
287 }
288
289 #[test]
290 fn test_headless_line() {
291 let mut line = HeadlessLine::new(0, false);
292 line.add_cell(Cell::new('H', 1));
293 line.add_cell(Cell::new('e', 1));
294 line.add_cell(Cell::new('你', 2));
295
296 assert_eq!(line.logical_line_index, 0);
297 assert!(!line.is_wrapped_part);
298 assert_eq!(line.cells.len(), 3);
299 assert_eq!(line.visual_width(), 4); }
301
302 #[test]
303 fn test_snapshot_generator_basic() {
304 let text = "Hello\nWorld\nRust";
305 let generator = SnapshotGenerator::from_text(text, 80);
306
307 assert_eq!(generator.line_count(), 3);
308 assert_eq!(generator.get_line(0), Some("Hello"));
309 assert_eq!(generator.get_line(1), Some("World"));
310 assert_eq!(generator.get_line(2), Some("Rust"));
311 }
312
313 #[test]
314 fn test_get_headless_grid() {
315 let text = "Line 1\nLine 2\nLine 3\nLine 4";
316 let generator = SnapshotGenerator::from_text(text, 80);
317
318 let grid = generator.get_headless_grid(0, 2);
320 assert_eq!(grid.start_visual_row, 0);
321 assert_eq!(grid.count, 2);
322 assert_eq!(grid.actual_line_count(), 2);
323
324 let line0 = &grid.lines[0];
326 assert_eq!(line0.logical_line_index, 0);
327 assert!(!line0.is_wrapped_part);
328 assert_eq!(line0.cells.len(), 6); let grid2 = generator.get_headless_grid(1, 2);
332 assert_eq!(grid2.actual_line_count(), 2);
333 assert_eq!(grid2.lines[0].logical_line_index, 1);
334 assert_eq!(grid2.lines[1].logical_line_index, 2);
335 }
336
337 #[test]
338 fn test_get_headless_grid_soft_wrap_single_line() {
339 let generator = SnapshotGenerator::from_text("abcd", 2);
340
341 let grid = generator.get_headless_grid(0, 10);
342 assert_eq!(grid.actual_line_count(), 2);
343
344 let line0_text: String = grid.lines[0].cells.iter().map(|c| c.ch).collect();
345 let line1_text: String = grid.lines[1].cells.iter().map(|c| c.ch).collect();
346
347 assert_eq!(grid.lines[0].logical_line_index, 0);
348 assert!(!grid.lines[0].is_wrapped_part);
349 assert_eq!(line0_text, "ab");
350
351 assert_eq!(grid.lines[1].logical_line_index, 0);
352 assert!(grid.lines[1].is_wrapped_part);
353 assert_eq!(line1_text, "cd");
354
355 let grid2 = generator.get_headless_grid(1, 1);
357 assert_eq!(grid2.actual_line_count(), 1);
358 assert_eq!(grid2.lines[0].logical_line_index, 0);
359 assert!(grid2.lines[0].is_wrapped_part);
360 let text2: String = grid2.lines[0].cells.iter().map(|c| c.ch).collect();
361 assert_eq!(text2, "cd");
362 }
363
364 #[test]
365 fn test_grid_with_cjk() {
366 let text = "Hello\n你好世界\nRust";
367 let generator = SnapshotGenerator::from_text(text, 80);
368
369 let grid = generator.get_headless_grid(1, 1);
370 let line = &grid.lines[0];
371
372 assert_eq!(line.cells.len(), 4); assert_eq!(line.visual_width(), 8); assert_eq!(line.cells[0].ch, '你');
377 assert_eq!(line.cells[0].width, 2);
378 assert_eq!(line.cells[1].ch, '好');
379 assert_eq!(line.cells[1].width, 2);
380 }
381
382 #[test]
383 fn test_grid_with_emoji() {
384 let text = "Hello 👋\nWorld 🌍";
385 let generator = SnapshotGenerator::from_text(text, 80);
386
387 let grid = generator.get_headless_grid(0, 2);
388 assert_eq!(grid.actual_line_count(), 2);
389
390 let line0 = &grid.lines[0];
392 assert_eq!(line0.cells.len(), 7); assert_eq!(line0.visual_width(), 8);
395 }
396
397 #[test]
398 fn test_grid_bounds() {
399 let text = "Line 1\nLine 2\nLine 3";
400 let generator = SnapshotGenerator::from_text(text, 80);
401
402 let grid = generator.get_headless_grid(1, 10);
404 assert_eq!(grid.actual_line_count(), 2); let grid2 = generator.get_headless_grid(10, 5);
409 assert_eq!(grid2.actual_line_count(), 0);
410 }
411
412 #[test]
413 fn test_empty_document() {
414 let generator = SnapshotGenerator::new(80);
415 let grid = generator.get_headless_grid(0, 10);
416 assert_eq!(grid.actual_line_count(), 1);
417 }
418
419 #[test]
420 fn test_viewport_width_change() {
421 let text = "Hello World";
422 let mut generator = SnapshotGenerator::from_text(text, 40);
423
424 assert_eq!(generator.viewport_width, 40);
425
426 generator.set_viewport_width(20);
427 assert_eq!(generator.viewport_width, 20);
428 generator.set_viewport_width(5);
430 let grid = generator.get_headless_grid(0, 10);
431 assert!(grid.actual_line_count() > 1);
432 }
433}