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 visual_in_logical: usize,
47 pub char_offset_start: usize,
49 pub char_offset_end: usize,
51 pub segment_x_start_cells: usize,
55 pub is_fold_placeholder_appended: bool,
57 pub cells: Vec<Cell>,
59}
60
61impl HeadlessLine {
62 pub fn new(logical_line_index: usize, is_wrapped_part: bool) -> Self {
64 Self {
65 logical_line_index,
66 is_wrapped_part,
67 visual_in_logical: if is_wrapped_part { 1 } else { 0 },
68 char_offset_start: 0,
69 char_offset_end: 0,
70 segment_x_start_cells: 0,
71 is_fold_placeholder_appended: false,
72 cells: Vec::new(),
73 }
74 }
75
76 pub fn set_visual_metadata(
78 &mut self,
79 visual_in_logical: usize,
80 char_offset_start: usize,
81 char_offset_end: usize,
82 segment_x_start_cells: usize,
83 ) {
84 self.visual_in_logical = visual_in_logical;
85 self.char_offset_start = char_offset_start;
86 self.char_offset_end = char_offset_end;
87 self.segment_x_start_cells = segment_x_start_cells;
88 }
89
90 pub fn set_fold_placeholder_appended(&mut self, appended: bool) {
92 self.is_fold_placeholder_appended = appended;
93 }
94
95 pub fn add_cell(&mut self, cell: Cell) {
97 self.cells.push(cell);
98 }
99
100 pub fn visual_width(&self) -> usize {
102 self.cells.iter().map(|c| c.width).sum()
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct HeadlessGrid {
109 pub lines: Vec<HeadlessLine>,
111 pub start_visual_row: usize,
113 pub count: usize,
115}
116
117impl HeadlessGrid {
118 pub fn new(start_visual_row: usize, count: usize) -> Self {
120 Self {
121 lines: Vec::new(),
122 start_visual_row,
123 count,
124 }
125 }
126
127 pub fn add_line(&mut self, line: HeadlessLine) {
129 self.lines.push(line);
130 }
131
132 pub fn actual_line_count(&self) -> usize {
134 self.lines.len()
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct MinimapLine {
141 pub logical_line_index: usize,
143 pub visual_in_logical: usize,
145 pub char_offset_start: usize,
147 pub char_offset_end: usize,
149 pub total_cells: usize,
151 pub non_whitespace_cells: usize,
153 pub dominant_style: Option<StyleId>,
155 pub is_fold_placeholder_appended: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct MinimapGrid {
162 pub lines: Vec<MinimapLine>,
164 pub start_visual_row: usize,
166 pub count: usize,
168}
169
170impl MinimapGrid {
171 pub fn new(start_visual_row: usize, count: usize) -> Self {
173 Self {
174 lines: Vec::new(),
175 start_visual_row,
176 count,
177 }
178 }
179
180 pub fn actual_line_count(&self) -> usize {
182 self.lines.len()
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct ComposedCell {
189 pub ch: char,
191 pub width: usize,
193 pub styles: Vec<crate::intervals::StyleId>,
195 pub source: ComposedCellSource,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ComposedCellSource {
202 Document {
204 offset: usize,
206 },
207 Virtual {
209 anchor_offset: usize,
211 },
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum ComposedLineKind {
217 Document {
219 logical_line: usize,
221 visual_in_logical: usize,
223 },
224 VirtualAboveLine {
226 logical_line: usize,
228 },
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ComposedLine {
234 pub kind: ComposedLineKind,
236 pub cells: Vec<ComposedCell>,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct ComposedGrid {
243 pub lines: Vec<ComposedLine>,
245 pub start_visual_row: usize,
247 pub count: usize,
249}
250
251impl ComposedGrid {
252 pub fn new(start_visual_row: usize, count: usize) -> Self {
254 Self {
255 lines: Vec::new(),
256 start_visual_row,
257 count,
258 }
259 }
260
261 pub fn actual_line_count(&self) -> usize {
263 self.lines.len()
264 }
265}
266
267pub struct SnapshotGenerator {
271 lines: Vec<String>,
273 viewport_width: usize,
275 tab_width: usize,
277 layout_engine: LayoutEngine,
279}
280
281impl SnapshotGenerator {
282 pub fn new(viewport_width: usize) -> Self {
284 let lines = vec![String::new()];
285 let mut layout_engine = LayoutEngine::new(viewport_width);
286 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
287 layout_engine.from_lines(&line_refs);
288
289 Self {
290 lines,
292 viewport_width,
293 tab_width: layout_engine.tab_width(),
294 layout_engine,
295 }
296 }
297
298 pub fn from_text(text: &str, viewport_width: usize) -> Self {
300 Self::from_text_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
301 }
302
303 pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
305 Self::from_text_with_options(text, viewport_width, tab_width, WrapMode::Char)
306 }
307
308 pub fn from_text_with_options(
310 text: &str,
311 viewport_width: usize,
312 tab_width: usize,
313 wrap_mode: WrapMode,
314 ) -> Self {
315 Self::from_text_with_layout_options(
316 text,
317 viewport_width,
318 tab_width,
319 wrap_mode,
320 WrapIndent::None,
321 )
322 }
323
324 pub fn from_text_with_layout_options(
326 text: &str,
327 viewport_width: usize,
328 tab_width: usize,
329 wrap_mode: WrapMode,
330 wrap_indent: WrapIndent,
331 ) -> Self {
332 let normalized = crate::text::normalize_crlf_to_lf(text);
333 let lines = crate::text::split_lines_preserve_trailing(normalized.as_ref());
334 let mut layout_engine = LayoutEngine::new(viewport_width);
335 layout_engine.set_tab_width(tab_width);
336 layout_engine.set_wrap_mode(wrap_mode);
337 layout_engine.set_wrap_indent(wrap_indent);
338 let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
339 layout_engine.from_lines(&line_refs);
340 Self {
341 lines,
342 viewport_width,
343 tab_width: layout_engine.tab_width(),
344 layout_engine,
345 }
346 }
347
348 pub fn set_lines(&mut self, lines: Vec<String>) {
350 self.lines = if lines.is_empty() {
351 vec![String::new()]
352 } else {
353 lines
354 };
355
356 let line_refs: Vec<&str> = self.lines.iter().map(|s| s.as_str()).collect();
357 self.layout_engine.from_lines(&line_refs);
358 }
359
360 pub fn set_viewport_width(&mut self, width: usize) {
362 self.viewport_width = width;
363 self.layout_engine.set_viewport_width(width);
364 }
365
366 pub fn set_tab_width(&mut self, tab_width: usize) {
368 self.tab_width = tab_width.max(1);
369 self.layout_engine.set_tab_width(self.tab_width);
370 }
371
372 pub fn tab_width(&self) -> usize {
374 self.tab_width
375 }
376
377 pub fn get_headless_grid(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
381 let mut grid = HeadlessGrid::new(start_visual_row, count);
382
383 if count == 0 {
384 return grid;
385 }
386
387 let total_visual = self.layout_engine.visual_line_count();
388 if start_visual_row >= total_visual {
389 return grid;
390 }
391
392 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
393 let mut current_visual = 0usize;
394
395 let mut line_start_offset = 0usize;
396 for logical_line in 0..self.layout_engine.logical_line_count() {
397 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
398 continue;
399 };
400
401 let line_text = self
402 .lines
403 .get(logical_line)
404 .map(|s| s.as_str())
405 .unwrap_or("");
406 let line_char_len = line_text.chars().count();
407
408 for visual_in_line in 0..layout.visual_line_count {
409 if current_visual >= end_visual {
410 return grid;
411 }
412
413 if current_visual >= start_visual_row {
414 let segment_start_col = if visual_in_line == 0 {
415 0
416 } else {
417 layout
418 .wrap_points
419 .get(visual_in_line - 1)
420 .map(|wp| wp.char_index)
421 .unwrap_or(0)
422 .min(line_char_len)
423 };
424
425 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
426 layout.wrap_points[visual_in_line]
427 .char_index
428 .min(line_char_len)
429 } else {
430 line_char_len
431 };
432
433 let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
434 let mut segment_x_start_cells = 0usize;
435 if visual_in_line > 0 {
436 let indent_cells = wrap_indent_cells_for_line_text(
437 line_text,
438 self.layout_engine.wrap_indent(),
439 self.viewport_width,
440 self.tab_width,
441 );
442 segment_x_start_cells = indent_cells;
443 for _ in 0..indent_cells {
444 headless_line.add_cell(Cell::new(' ', 1));
445 }
446 }
447 let seg_start_x_in_line =
448 visual_x_for_column(line_text, segment_start_col, self.tab_width);
449 let mut x_in_line = seg_start_x_in_line;
450 for ch in line_text
451 .chars()
452 .skip(segment_start_col)
453 .take(segment_end_col.saturating_sub(segment_start_col))
454 {
455 let w = cell_width_at(ch, x_in_line, self.tab_width);
456 x_in_line = x_in_line.saturating_add(w);
457 headless_line.add_cell(Cell::new(ch, w));
458 }
459 headless_line.set_visual_metadata(
460 visual_in_line,
461 line_start_offset.saturating_add(segment_start_col),
462 line_start_offset.saturating_add(segment_end_col),
463 segment_x_start_cells,
464 );
465
466 grid.add_line(headless_line);
467 }
468
469 current_visual = current_visual.saturating_add(1);
470 }
471
472 line_start_offset = line_start_offset.saturating_add(line_char_len);
473 if logical_line + 1 < self.layout_engine.logical_line_count() {
474 line_start_offset = line_start_offset.saturating_add(1);
475 }
476 }
477
478 grid
479 }
480
481 pub fn get_line(&self, line_index: usize) -> Option<&str> {
483 self.lines.get(line_index).map(|s| s.as_str())
484 }
485
486 pub fn line_count(&self) -> usize {
488 self.lines.len()
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_cell_creation() {
498 let cell = Cell::new('a', 1);
499 assert_eq!(cell.ch, 'a');
500 assert_eq!(cell.width, 1);
501 assert!(cell.styles.is_empty());
502 }
503
504 #[test]
505 fn test_cell_with_styles() {
506 let cell = Cell::with_styles('你', 2, vec![1, 2, 3]);
507 assert_eq!(cell.ch, '你');
508 assert_eq!(cell.width, 2);
509 assert_eq!(cell.styles, vec![1, 2, 3]);
510 }
511
512 #[test]
513 fn test_headless_line() {
514 let mut line = HeadlessLine::new(0, false);
515 line.add_cell(Cell::new('H', 1));
516 line.add_cell(Cell::new('e', 1));
517 line.add_cell(Cell::new('你', 2));
518
519 assert_eq!(line.logical_line_index, 0);
520 assert!(!line.is_wrapped_part);
521 assert_eq!(line.visual_in_logical, 0);
522 assert_eq!(line.char_offset_start, 0);
523 assert_eq!(line.char_offset_end, 0);
524 assert_eq!(line.segment_x_start_cells, 0);
525 assert!(!line.is_fold_placeholder_appended);
526 assert_eq!(line.cells.len(), 3);
527 assert_eq!(line.visual_width(), 4); }
529
530 #[test]
531 fn test_snapshot_generator_basic() {
532 let text = "Hello\nWorld\nRust";
533 let generator = SnapshotGenerator::from_text(text, 80);
534
535 assert_eq!(generator.line_count(), 3);
536 assert_eq!(generator.get_line(0), Some("Hello"));
537 assert_eq!(generator.get_line(1), Some("World"));
538 assert_eq!(generator.get_line(2), Some("Rust"));
539 }
540
541 #[test]
542 fn test_get_headless_grid() {
543 let text = "Line 1\nLine 2\nLine 3\nLine 4";
544 let generator = SnapshotGenerator::from_text(text, 80);
545
546 let grid = generator.get_headless_grid(0, 2);
548 assert_eq!(grid.start_visual_row, 0);
549 assert_eq!(grid.count, 2);
550 assert_eq!(grid.actual_line_count(), 2);
551
552 let line0 = &grid.lines[0];
554 assert_eq!(line0.logical_line_index, 0);
555 assert!(!line0.is_wrapped_part);
556 assert_eq!(line0.visual_in_logical, 0);
557 assert_eq!(line0.char_offset_start, 0);
558 assert_eq!(line0.char_offset_end, 6);
559 assert_eq!(line0.cells.len(), 6); let grid2 = generator.get_headless_grid(1, 2);
563 assert_eq!(grid2.actual_line_count(), 2);
564 assert_eq!(grid2.lines[0].logical_line_index, 1);
565 assert_eq!(grid2.lines[1].logical_line_index, 2);
566 }
567
568 #[test]
569 fn test_get_headless_grid_soft_wrap_single_line() {
570 let generator = SnapshotGenerator::from_text("abcd", 2);
571
572 let grid = generator.get_headless_grid(0, 10);
573 assert_eq!(grid.actual_line_count(), 2);
574
575 let line0_text: String = grid.lines[0].cells.iter().map(|c| c.ch).collect();
576 let line1_text: String = grid.lines[1].cells.iter().map(|c| c.ch).collect();
577
578 assert_eq!(grid.lines[0].logical_line_index, 0);
579 assert!(!grid.lines[0].is_wrapped_part);
580 assert_eq!(grid.lines[0].visual_in_logical, 0);
581 assert_eq!(line0_text, "ab");
582
583 assert_eq!(grid.lines[1].logical_line_index, 0);
584 assert!(grid.lines[1].is_wrapped_part);
585 assert_eq!(grid.lines[1].visual_in_logical, 1);
586 assert_eq!(line1_text, "cd");
587
588 let grid2 = generator.get_headless_grid(1, 1);
590 assert_eq!(grid2.actual_line_count(), 1);
591 assert_eq!(grid2.lines[0].logical_line_index, 0);
592 assert!(grid2.lines[0].is_wrapped_part);
593 let text2: String = grid2.lines[0].cells.iter().map(|c| c.ch).collect();
594 assert_eq!(text2, "cd");
595 }
596
597 #[test]
598 fn test_grid_with_cjk() {
599 let text = "Hello\n你好世界\nRust";
600 let generator = SnapshotGenerator::from_text(text, 80);
601
602 let grid = generator.get_headless_grid(1, 1);
603 let line = &grid.lines[0];
604
605 assert_eq!(line.cells.len(), 4); assert_eq!(line.visual_width(), 8); assert_eq!(line.cells[0].ch, '你');
610 assert_eq!(line.cells[0].width, 2);
611 assert_eq!(line.cells[1].ch, '好');
612 assert_eq!(line.cells[1].width, 2);
613 }
614
615 #[test]
616 fn test_grid_with_emoji() {
617 let text = "Hello 👋\nWorld 🌍";
618 let generator = SnapshotGenerator::from_text(text, 80);
619
620 let grid = generator.get_headless_grid(0, 2);
621 assert_eq!(grid.actual_line_count(), 2);
622
623 let line0 = &grid.lines[0];
625 assert_eq!(line0.cells.len(), 7); assert_eq!(line0.visual_width(), 8);
628 }
629
630 #[test]
631 fn test_grid_bounds() {
632 let text = "Line 1\nLine 2\nLine 3";
633 let generator = SnapshotGenerator::from_text(text, 80);
634
635 let grid = generator.get_headless_grid(1, 10);
637 assert_eq!(grid.actual_line_count(), 2); let grid2 = generator.get_headless_grid(10, 5);
642 assert_eq!(grid2.actual_line_count(), 0);
643 }
644
645 #[test]
646 fn test_empty_document() {
647 let generator = SnapshotGenerator::new(80);
648 let grid = generator.get_headless_grid(0, 10);
649 assert_eq!(grid.actual_line_count(), 1);
650 }
651
652 #[test]
653 fn test_viewport_width_change() {
654 let text = "Hello World";
655 let mut generator = SnapshotGenerator::from_text(text, 40);
656
657 assert_eq!(generator.viewport_width, 40);
658
659 generator.set_viewport_width(20);
660 assert_eq!(generator.viewport_width, 20);
661 generator.set_viewport_width(5);
663 let grid = generator.get_headless_grid(0, 10);
664 assert!(grid.actual_line_count() > 1);
665 }
666}