1use super::*;
4
5impl EditorCore {
6 pub fn get_headless_grid_styled(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
14 self.with_visual_row_index(|index| {
15 let mut grid = HeadlessGrid::new(start_visual_row, count);
16 if count == 0 {
17 return grid;
18 }
19
20 let total_visual = index.total_visual_lines();
21 if start_visual_row >= total_visual {
22 return grid;
23 }
24
25 let tab_width = self.layout_engine.tab_width();
26 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
27 let regions = self.folding_manager.regions();
28
29 let Some((mut span, mut visual_in_line)) = index.span_for_visual_row(start_visual_row)
30 else {
31 return grid;
32 };
33 let mut current_visual = start_visual_row;
34
35 while current_visual < end_visual {
36 let logical_line = span.logical_line;
37
38 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
39 let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
40 current_visual = current_visual.saturating_add(remaining_in_span);
41 let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
42 break;
43 };
44 span = next_span;
45 visual_in_line = 0;
46 continue;
47 };
48
49 let line_text = self
50 .line_index
51 .get_line_text(logical_line)
52 .unwrap_or_default();
53 let line_char_len = line_text.chars().count();
54 let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
55
56 let segment_start_col = if visual_in_line == 0 {
57 0
58 } else {
59 layout
60 .wrap_points
61 .get(visual_in_line - 1)
62 .map(|wp| wp.char_index)
63 .unwrap_or(0)
64 .min(line_char_len)
65 };
66
67 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
68 layout.wrap_points[visual_in_line]
69 .char_index
70 .min(line_char_len)
71 } else {
72 line_char_len
73 };
74
75 let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
76 let mut segment_x_start_cells = 0usize;
77 if visual_in_line > 0 {
78 let indent_cells = wrap_indent_cells_for_line_text(
79 &line_text,
80 self.layout_engine.wrap_indent(),
81 self.viewport_width,
82 tab_width,
83 );
84 segment_x_start_cells = indent_cells;
85 for _ in 0..indent_cells {
86 headless_line.add_cell(Cell::new(' ', 1));
87 }
88 }
89 let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
90
91 for (col, ch) in line_text
92 .chars()
93 .enumerate()
94 .skip(segment_start_col)
95 .take(segment_end_col.saturating_sub(segment_start_col))
96 {
97 let offset = line_start_offset + col;
98 let styles = self.styles_at_offset(offset);
99 let w = cell_width_at(ch, x_in_line, tab_width);
100 x_in_line = x_in_line.saturating_add(w);
101 headless_line.add_cell(Cell::with_styles(ch, w, styles));
102 }
103
104 headless_line.set_visual_metadata(
105 visual_in_line,
106 line_start_offset.saturating_add(segment_start_col),
107 line_start_offset.saturating_add(segment_end_col),
108 segment_x_start_cells,
109 );
110 headless_line.set_fold_placeholder_appended(false);
111
112 if visual_in_line + 1 == layout.visual_line_count
114 && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
115 && !region.placeholder.is_empty()
116 {
117 if !headless_line.cells.is_empty() {
118 x_in_line = x_in_line.saturating_add(char_width(' '));
119 headless_line.add_cell(Cell::with_styles(
120 ' ',
121 char_width(' '),
122 vec![FOLD_PLACEHOLDER_STYLE_ID],
123 ));
124 }
125 for ch in region.placeholder.chars() {
126 let w = cell_width_at(ch, x_in_line, tab_width);
127 x_in_line = x_in_line.saturating_add(w);
128 headless_line.add_cell(Cell::with_styles(
129 ch,
130 w,
131 vec![FOLD_PLACEHOLDER_STYLE_ID],
132 ));
133 }
134 if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
135 x_in_line = x_in_line.saturating_add(char_width(' '));
136 headless_line.add_cell(Cell::with_styles(
137 ' ',
138 char_width(' '),
139 vec![FOLD_PLACEHOLDER_STYLE_ID],
140 ));
141
142 let w = cell_width_at(right_bracket, x_in_line, tab_width);
143 x_in_line = x_in_line.saturating_add(w);
144 headless_line.add_cell(Cell::with_styles(
145 right_bracket,
146 w,
147 vec![FOLD_PLACEHOLDER_STYLE_ID],
148 ));
149 }
150 headless_line.set_fold_placeholder_appended(true);
151 }
152
153 grid.add_line(headless_line);
154 current_visual = current_visual.saturating_add(1);
155 visual_in_line = visual_in_line.saturating_add(1);
156 if visual_in_line >= span.visual_line_count {
157 let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
158 break;
159 };
160 span = next_span;
161 visual_in_line = 0;
162 }
163 }
164
165 grid
166 })
167 }
168
169 pub fn get_minimap_grid(&self, start_visual_row: usize, count: usize) -> MinimapGrid {
174 self.with_visual_row_index(|index| {
175 let mut grid = MinimapGrid::new(start_visual_row, count);
176 if count == 0 {
177 return grid;
178 }
179
180 let total_visual = index.total_visual_lines();
181 if start_visual_row >= total_visual {
182 return grid;
183 }
184
185 let tab_width = self.layout_engine.tab_width();
186 let end_visual = start_visual_row.saturating_add(count).min(total_visual);
187 let regions = self.folding_manager.regions();
188
189 let Some((mut span, mut visual_in_line)) = index.span_for_visual_row(start_visual_row)
190 else {
191 return grid;
192 };
193 let mut current_visual = start_visual_row;
194
195 while current_visual < end_visual {
196 let logical_line = span.logical_line;
197
198 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
199 let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
200 current_visual = current_visual.saturating_add(remaining_in_span);
201 let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
202 break;
203 };
204 span = next_span;
205 visual_in_line = 0;
206 continue;
207 };
208
209 let line_text = self
210 .line_index
211 .get_line_text(logical_line)
212 .unwrap_or_default();
213 let line_char_len = line_text.chars().count();
214 let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
215
216 let segment_start_col = if visual_in_line == 0 {
217 0
218 } else {
219 layout
220 .wrap_points
221 .get(visual_in_line - 1)
222 .map(|wp| wp.char_index)
223 .unwrap_or(0)
224 .min(line_char_len)
225 };
226
227 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
228 layout.wrap_points[visual_in_line]
229 .char_index
230 .min(line_char_len)
231 } else {
232 line_char_len
233 };
234
235 let mut total_cells = 0usize;
236 let mut non_whitespace_cells = 0usize;
237 let mut dominant_style_counts: HashMap<StyleId, usize> = HashMap::new();
238 if visual_in_line > 0 {
239 let indent_cells = wrap_indent_cells_for_line_text(
240 &line_text,
241 self.layout_engine.wrap_indent(),
242 self.viewport_width,
243 tab_width,
244 );
245 total_cells = total_cells.saturating_add(indent_cells);
246 }
247 let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
248
249 for (col, ch) in line_text
250 .chars()
251 .enumerate()
252 .skip(segment_start_col)
253 .take(segment_end_col.saturating_sub(segment_start_col))
254 {
255 let offset = line_start_offset + col;
256 let styles = self.styles_at_offset(offset);
257 let w = cell_width_at(ch, x_in_line, tab_width);
258 x_in_line = x_in_line.saturating_add(w);
259 total_cells = total_cells.saturating_add(w);
260 if !ch.is_whitespace() {
261 non_whitespace_cells = non_whitespace_cells.saturating_add(w);
262 }
263 if let Some(style) = styles.first().copied() {
264 let entry = dominant_style_counts.entry(style).or_insert(0);
265 *entry = entry.saturating_add(w);
266 }
267 }
268
269 let mut placeholder_appended = false;
270 if visual_in_line + 1 == layout.visual_line_count
271 && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
272 && !region.placeholder.is_empty()
273 {
274 placeholder_appended = true;
275 if total_cells > 0 {
276 x_in_line = x_in_line.saturating_add(char_width(' '));
277 total_cells = total_cells.saturating_add(char_width(' '));
278 }
279 for ch in region.placeholder.chars() {
280 let w = cell_width_at(ch, x_in_line, tab_width);
281 x_in_line = x_in_line.saturating_add(w);
282 total_cells = total_cells.saturating_add(w);
283 if !ch.is_whitespace() {
284 non_whitespace_cells = non_whitespace_cells.saturating_add(w);
285 }
286 let entry = dominant_style_counts
287 .entry(FOLD_PLACEHOLDER_STYLE_ID)
288 .or_insert(0);
289 *entry = entry.saturating_add(w);
290 }
291 if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
292 x_in_line = x_in_line.saturating_add(char_width(' '));
293 total_cells = total_cells.saturating_add(char_width(' '));
294 let entry = dominant_style_counts
295 .entry(FOLD_PLACEHOLDER_STYLE_ID)
296 .or_insert(0);
297 *entry = entry.saturating_add(char_width(' '));
298
299 let w = cell_width_at(right_bracket, x_in_line, tab_width);
300 x_in_line = x_in_line.saturating_add(w);
301 total_cells = total_cells.saturating_add(w);
302 non_whitespace_cells = non_whitespace_cells.saturating_add(w);
303 let entry = dominant_style_counts
304 .entry(FOLD_PLACEHOLDER_STYLE_ID)
305 .or_insert(0);
306 *entry = entry.saturating_add(w);
307 }
308 }
309
310 let dominant_style = dominant_style_counts
311 .into_iter()
312 .max_by(|a, b| a.1.cmp(&b.1).then_with(|| b.0.cmp(&a.0)))
313 .map(|(style, _)| style);
314
315 grid.lines.push(MinimapLine {
316 logical_line_index: logical_line,
317 visual_in_logical: visual_in_line,
318 char_offset_start: line_start_offset.saturating_add(segment_start_col),
319 char_offset_end: line_start_offset.saturating_add(segment_end_col),
320 total_cells,
321 non_whitespace_cells,
322 dominant_style,
323 is_fold_placeholder_appended: placeholder_appended,
324 });
325
326 current_visual = current_visual.saturating_add(1);
327 visual_in_line = visual_in_line.saturating_add(1);
328 if visual_in_line >= span.visual_line_count {
329 let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
330 break;
331 };
332 span = next_span;
333 visual_in_line = 0;
334 }
335 }
336
337 grid
338 })
339 }
340
341 pub fn get_headless_grid_composed(
353 &self,
354 start_visual_row: usize,
355 count: usize,
356 ) -> ComposedGrid {
357 let mut grid = ComposedGrid::new(start_visual_row, count);
358 if count == 0 {
359 return grid;
360 }
361
362 #[derive(Debug, Clone)]
363 struct VirtualText {
364 anchor: usize,
365 text: String,
366 styles: Vec<StyleId>,
367 }
368
369 let mut inline_before: HashMap<usize, Vec<VirtualText>> = HashMap::new();
371 let mut inline_after: HashMap<usize, Vec<VirtualText>> = HashMap::new();
372 let mut above_by_line: BTreeMap<usize, Vec<VirtualText>> = BTreeMap::new();
373
374 for decorations in self.decorations.values() {
375 for deco in decorations {
376 let Some(text) = deco.text.as_ref() else {
377 continue;
378 };
379 if text.is_empty() {
380 continue;
381 }
382
383 let anchor = match deco.placement {
384 DecorationPlacement::After => deco.range.end,
385 DecorationPlacement::Before | DecorationPlacement::AboveLine => {
386 deco.range.start
387 }
388 };
389 let vt = VirtualText {
390 anchor,
391 text: text.clone(),
392 styles: deco.styles.clone(),
393 };
394
395 match deco.placement {
396 DecorationPlacement::Before => {
397 inline_before.entry(anchor).or_default().push(vt);
398 }
399 DecorationPlacement::After => {
400 inline_after.entry(anchor).or_default().push(vt);
401 }
402 DecorationPlacement::AboveLine => {
403 let line = self.line_index.char_offset_to_position(anchor).0;
404 above_by_line.entry(line).or_default().push(vt);
405 }
406 }
407 }
408 }
409
410 let regions = self.folding_manager.regions();
411 let line_count = self.layout_engine.logical_line_count();
412 let mut above_lines = Vec::new();
413 let mut above_prefix_counts = vec![0usize];
414 let mut total_above = 0usize;
415 for (&line, above) in &above_by_line {
416 if line >= line_count || Self::is_logical_line_hidden(regions, line) {
417 continue;
418 }
419 above_lines.push(line);
420 total_above = total_above.saturating_add(above.len());
421 above_prefix_counts.push(total_above);
422 }
423
424 let above_count_before_line = |line: usize| -> usize {
425 let idx = above_lines.partition_point(|candidate| *candidate < line);
426 above_prefix_counts[idx]
427 };
428
429 let (total_composed, start_logical_line, mut current_visual) =
430 self.with_visual_row_index(|index| {
431 let total_composed = index.total_visual_lines().saturating_add(total_above);
432 if line_count == 0 || start_visual_row >= total_composed {
433 return (total_composed, line_count, total_composed);
434 }
435
436 let composed_rows_before_line = |line: usize| -> usize {
437 index
438 .visual_rows_before_logical_line(line)
439 .saturating_add(above_count_before_line(line))
440 };
441
442 let mut low = 0usize;
443 let mut high = line_count;
444 while low < high {
445 let mid = (low + high).div_ceil(2);
446 if composed_rows_before_line(mid) <= start_visual_row {
447 low = mid;
448 } else {
449 high = mid.saturating_sub(1);
450 }
451 }
452
453 let mut start_line = low.min(line_count.saturating_sub(1));
454 if index.span_for_logical_line(start_line).is_none() {
455 let doc_visual = index.visual_rows_before_logical_line(start_line);
456 let Some((span, _)) = index.span_for_visual_row(doc_visual) else {
457 return (total_composed, line_count, total_composed);
458 };
459 start_line = span.logical_line;
460 }
461
462 (
463 total_composed,
464 start_line,
465 composed_rows_before_line(start_line),
466 )
467 });
468
469 if start_visual_row >= total_composed {
470 return grid;
471 }
472
473 let end_visual = start_visual_row.saturating_add(count).min(total_composed);
474 let tab_width = self.layout_engine.tab_width();
475
476 for logical_line in start_logical_line..line_count {
477 if Self::is_logical_line_hidden(regions, logical_line) {
478 continue;
479 }
480
481 if let Some(above) = above_by_line.get(&logical_line) {
483 for vt in above {
484 if current_visual >= end_visual {
485 return grid;
486 }
487
488 if current_visual >= start_visual_row {
489 let mut x_render = 0usize;
490 let mut cells: Vec<ComposedCell> = Vec::new();
491 for ch in vt.text.chars() {
492 let w = cell_width_at(ch, x_render, tab_width);
493 x_render = x_render.saturating_add(w);
494 cells.push(ComposedCell {
495 ch,
496 width: w,
497 styles: vt.styles.clone(),
498 source: ComposedCellSource::Virtual {
499 anchor_offset: vt.anchor,
500 },
501 });
502 }
503
504 grid.lines.push(ComposedLine {
505 kind: ComposedLineKind::VirtualAboveLine { logical_line },
506 char_offset_start: vt.anchor,
507 char_offset_end: vt.anchor,
508 cells,
509 });
510 }
511
512 current_visual = current_visual.saturating_add(1);
513 }
514 }
515
516 let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
517 continue;
518 };
519
520 let line_text = self
521 .line_index
522 .get_line_text(logical_line)
523 .unwrap_or_default();
524 let line_char_len = line_text.chars().count();
525 let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
526
527 for visual_in_line in 0..layout.visual_line_count {
528 if current_visual >= end_visual {
529 return grid;
530 }
531
532 if current_visual < start_visual_row {
533 current_visual = current_visual.saturating_add(1);
534 continue;
535 }
536
537 let segment_start_col = if visual_in_line == 0 {
538 0
539 } else {
540 layout
541 .wrap_points
542 .get(visual_in_line - 1)
543 .map(|wp| wp.char_index)
544 .unwrap_or(0)
545 .min(line_char_len)
546 };
547
548 let segment_end_col = if visual_in_line < layout.wrap_points.len() {
549 layout.wrap_points[visual_in_line]
550 .char_index
551 .min(line_char_len)
552 } else {
553 line_char_len
554 };
555
556 let segment_start_offset = line_start_offset + segment_start_col;
557
558 let mut cells: Vec<ComposedCell> = Vec::new();
559
560 let mut x_render = 0usize;
561 if visual_in_line > 0 {
562 let indent_cells = wrap_indent_cells_for_line_text(
563 &line_text,
564 self.layout_engine.wrap_indent(),
565 self.viewport_width,
566 tab_width,
567 );
568 x_render = x_render.saturating_add(indent_cells);
569 for _ in 0..indent_cells {
570 cells.push(ComposedCell {
571 ch: ' ',
572 width: 1,
573 styles: Vec::new(),
574 source: ComposedCellSource::Virtual {
575 anchor_offset: segment_start_offset,
576 },
577 });
578 }
579 }
580
581 let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
582
583 let push_virtual = |anchor: usize,
584 list: &[VirtualText],
585 cells: &mut Vec<ComposedCell>,
586 x_render: &mut usize| {
587 for vt in list {
588 for ch in vt.text.chars() {
589 let w = cell_width_at(ch, *x_render, tab_width);
590 *x_render = x_render.saturating_add(w);
591 cells.push(ComposedCell {
592 ch,
593 width: w,
594 styles: vt.styles.clone(),
595 source: ComposedCellSource::Virtual {
596 anchor_offset: anchor,
597 },
598 });
599 }
600 }
601 };
602
603 for (col, ch) in line_text
604 .chars()
605 .enumerate()
606 .skip(segment_start_col)
607 .take(segment_end_col.saturating_sub(segment_start_col))
608 {
609 let offset = line_start_offset + col;
610
611 if let Some(list) = inline_before.get(&offset) {
612 push_virtual(offset, list, &mut cells, &mut x_render);
613 }
614 if let Some(list) = inline_after.get(&offset) {
615 push_virtual(offset, list, &mut cells, &mut x_render);
616 }
617
618 let styles = self.styles_at_offset(offset);
619 let w = cell_width_at(ch, x_in_line, tab_width);
620 x_in_line = x_in_line.saturating_add(w);
621 x_render = x_render.saturating_add(w);
622 cells.push(ComposedCell {
623 ch,
624 width: w,
625 styles,
626 source: ComposedCellSource::Document { offset },
627 });
628 }
629
630 if visual_in_line + 1 == layout.visual_line_count {
632 let eol_offset = line_start_offset + line_char_len;
633 if let Some(list) = inline_before.get(&eol_offset) {
634 push_virtual(eol_offset, list, &mut cells, &mut x_render);
635 }
636 if let Some(list) = inline_after.get(&eol_offset) {
637 push_virtual(eol_offset, list, &mut cells, &mut x_render);
638 }
639
640 if let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
642 && !region.placeholder.is_empty()
643 {
644 if !cells.is_empty() {
645 x_render = x_render.saturating_add(char_width(' '));
646 cells.push(ComposedCell {
647 ch: ' ',
648 width: char_width(' '),
649 styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
650 source: ComposedCellSource::Virtual {
651 anchor_offset: eol_offset,
652 },
653 });
654 }
655 for ch in region.placeholder.chars() {
656 let w = cell_width_at(ch, x_render, tab_width);
657 x_render = x_render.saturating_add(w);
658 cells.push(ComposedCell {
659 ch,
660 width: w,
661 styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
662 source: ComposedCellSource::Virtual {
663 anchor_offset: eol_offset,
664 },
665 });
666 }
667 if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
668 x_render = x_render.saturating_add(char_width(' '));
669 cells.push(ComposedCell {
670 ch: ' ',
671 width: char_width(' '),
672 styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
673 source: ComposedCellSource::Virtual {
674 anchor_offset: eol_offset,
675 },
676 });
677
678 let w = cell_width_at(right_bracket, x_render, tab_width);
679 cells.push(ComposedCell {
680 ch: right_bracket,
681 width: w,
682 styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
683 source: ComposedCellSource::Virtual {
684 anchor_offset: eol_offset,
685 },
686 });
687 }
688 }
689 }
690
691 grid.lines.push(ComposedLine {
692 kind: ComposedLineKind::Document {
693 logical_line,
694 visual_in_logical: visual_in_line,
695 },
696 char_offset_start: segment_start_offset,
697 char_offset_end: line_start_offset + segment_end_col,
698 cells,
699 });
700
701 current_visual = current_visual.saturating_add(1);
702 }
703 }
704
705 grid
706 }
707}