@@ -965,6 +965,10 @@ impl DocLayout {
965965 // whole document (the headless/first-open path).
966966 anchor_line : usize ,
967967 viewport_h : f32 ,
968+ // Sorted, disjoint line ranges to collapse to zero height (heading folds). Hidden
969+ // lines are skipped: never materialized, `heights[i] = 0`, so the prefix-sum `tops`
970+ // ties across them and the whole virtualization stack excludes them for free.
971+ folds : & [ Range < usize > ] ,
968972 ) -> Self {
969973 let LayoutParams {
970974 content_x0,
@@ -1036,6 +1040,7 @@ impl DocLayout {
10361040 let mut past_band = false ; // true once the band is tall enough; tail is estimated
10371041 let mut measured_start = n; // first materialized line (n = none materialized yet)
10381042 let mut measured_count = n; // first estimated tail line (n = materialized to end)
1043+ let mut fold_idx = 0usize ; // moving cursor into the sorted `folds` ranges
10391044 let mut preedit_caret: Option < ( usize , usize ) > = None ;
10401045 // Bucket inline styles per line once (O(n + styles)) instead of the O(n²)
10411046 // per-line `styles_in_range` scan — the dominant per-keystroke cost on large
@@ -1082,6 +1087,25 @@ impl DocLayout {
10821087 // `i` indexes several parallel per-line inputs (styles, markers, diff).
10831088 #[ allow( clippy:: needless_range_loop) ]
10841089 for i in 0 ..n {
1090+ // Folded line: zero height, no materialization, no band accounting. Placed
1091+ // before the estimate/band logic so a fold never perturbs the measured band.
1092+ while fold_idx < folds. len ( ) && folds[ fold_idx] . end <= i {
1093+ fold_idx += 1 ;
1094+ }
1095+ if fold_idx < folds. len ( ) && folds[ fold_idx] . start <= i {
1096+ heights. push ( 0.0 ) ;
1097+ layouts. push ( empty_layout. clone ( ) ) ;
1098+ renders. push ( empty_render. clone ( ) ) ;
1099+ line_ranges. push ( snapshot. line_byte_range ( i) ) ;
1100+ line_diffs. push ( LineDiff :: default ( ) ) ;
1101+ ghosts. push ( Vec :: new ( ) ) ;
1102+ ghost_height. push ( 0.0 ) ;
1103+ quote_bars. push ( Vec :: new ( ) ) ;
1104+ image_blocks. push ( None ) ;
1105+ inline_draws. push ( Vec :: new ( ) ) ;
1106+ table_lines. push ( None ) ;
1107+ continue ;
1108+ }
10851109 // Estimate the head (above the band) and the tail (once the band has covered
10861110 // the viewport + overscan); materialize only the band in between.
10871111 if i >= measure_from_line && !past_band && measured_start == n {
@@ -1403,6 +1427,23 @@ impl DocLayout {
14031427 . min ( n - 1 )
14041428 }
14051429
1430+ /// Screen-space top y (device px) of line `i`'s real text, or `None` if out of
1431+ /// range. Used to place fold chevrons in the gutter beside heading lines.
1432+ pub fn line_top_screen ( & self , line : usize ) -> Option < f32 > {
1433+ ( line < self . layouts . len ( ) ) . then ( || self . real_top ( line) - self . scroll_y )
1434+ }
1435+
1436+ /// Height (device px) of line `i`'s laid-out text block, for vertically centering
1437+ /// gutter affordances against it. Zero for estimated/hidden lines.
1438+ pub fn line_text_height ( & self , line : usize ) -> f32 {
1439+ self . layouts . get ( line) . map ( |l| l. height ( ) ) . unwrap_or ( 0.0 )
1440+ }
1441+
1442+ /// Left draw origin (device px) of the document body — the gutter ends here.
1443+ pub fn body_left ( & self ) -> f32 {
1444+ self . pad_x
1445+ }
1446+
14061447 /// Top y (device px, before scroll) of the *real* text of line `i` — i.e. below
14071448 /// any ghost (deleted) rows stacked above it.
14081449 fn real_top ( & self , line : usize ) -> f32 {
@@ -2277,6 +2318,7 @@ mod tests {
22772318 None ,
22782319 0 ,
22792320 f32:: INFINITY ,
2321+ & [ ] ,
22802322 ) ;
22812323 // Several buffer offsets (incl. second line) should map to a caret rect
22822324 // whose center hit-tests back to the same offset.
@@ -2289,6 +2331,55 @@ mod tests {
22892331 }
22902332 }
22912333
2334+ /// Folding integration: hidden lines get zero height, so `tops` ties across the
2335+ /// folded run, `content_height` drops by exactly the folded lines' heights, and a
2336+ /// click just below the fold resolves to the first visible line after it. This is
2337+ /// the load-bearing virtualization invariant the plan flags as riskiest.
2338+ #[ test]
2339+ fn folded_lines_are_zero_height ( ) {
2340+ use crate :: buffer:: Buffer ;
2341+ let mut engine = TextEngine :: new ( ) ;
2342+ let theme = EditorTheme :: dracula ( ) ;
2343+ let mut buffer: Buffer = "line0\n line1\n line2\n line3\n line4\n " . parse ( ) . unwrap ( ) ;
2344+ let snapshot = buffer. render_snapshot ( ) ;
2345+ let params = test_params ( & theme, 1200.0 ) ;
2346+ let build = |engine : & mut TextEngine , folds : & [ Range < usize > ] | {
2347+ DocLayout :: build (
2348+ engine,
2349+ & mut LineCache :: new ( ) ,
2350+ & mut RenderCache :: new ( ) ,
2351+ & mut HeightCache :: new ( ) ,
2352+ & mut TableCache :: new ( ) ,
2353+ 0 ,
2354+ & snapshot,
2355+ & theme,
2356+ None ,
2357+ None ,
2358+ & ImageCache :: new ( ) ,
2359+ 0 ,
2360+ & params,
2361+ None ,
2362+ 0 ,
2363+ f32:: INFINITY ,
2364+ folds,
2365+ )
2366+ } ;
2367+ let full = build ( & mut engine, & [ ] ) ;
2368+ let folded = build ( & mut engine, & [ 1 ..3 ] ) ; // hide lines 1 and 2
2369+
2370+ // Lines 1 and 2 collapse: their tops tie, and content height drops by exactly
2371+ // the height those two lines occupied in the unfolded layout.
2372+ assert_eq ! ( folded. tops[ 1 ] , folded. tops[ 2 ] ) ;
2373+ assert_eq ! ( folded. tops[ 2 ] , folded. tops[ 3 ] ) ;
2374+ let hidden_h = full. tops [ 3 ] - full. tops [ 1 ] ;
2375+ assert ! ( ( full. content_height( ) - folded. content_height( ) - hidden_h) . abs( ) < 1e-3 ) ;
2376+
2377+ // A click at the y where line 3 now sits resolves to line 3 (not a hidden line).
2378+ let y3 = folded. line_top_screen ( 3 ) . expect ( "line 3 visible" ) + 1.0 ;
2379+ let hit = folded. hit_test ( folded. pad_x + 1.0 , y3) . expect ( "hit" ) ;
2380+ assert_eq ! ( folded. line_of( hit) , 3 ) ;
2381+ }
2382+
22922383 /// The outline-panel inset: shrinking the horizontal content region to
22932384 /// `content_w = W - OUTLINE_WIDTH` narrows the doc's draw origin / body width and
22942385 /// caps `self.width` at the region edge (not the window edge), so full-width diff
@@ -2333,6 +2424,7 @@ mod tests {
23332424 None ,
23342425 0 ,
23352426 f32:: INFINITY ,
2427+ & [ ] ,
23362428 )
23372429 } ;
23382430 let doc = build ( & params) ;
@@ -2394,6 +2486,7 @@ mod tests {
23942486 None ,
23952487 0 ,
23962488 f32:: INFINITY ,
2489+ & [ ] ,
23972490 ) ;
23982491
23992492 // A mid-document line: its top pins to the viewport top exactly.
@@ -2444,6 +2537,7 @@ mod tests {
24442537 None ,
24452538 0 ,
24462539 f32:: INFINITY ,
2540+ & [ ] ,
24472541 ) ;
24482542 // Line 0's changed version has a deleted ghost stacked above it.
24492543 assert ! ( doc. ghost_height[ 0 ] > 0.0 , "expected a ghost above line 0" ) ;
@@ -2490,6 +2584,7 @@ mod tests {
24902584 None ,
24912585 0 ,
24922586 f32:: INFINITY ,
2587+ & [ ] ,
24932588 ) ;
24942589 assert ! (
24952590 !doc. trailing_ghosts. is_empty( ) ,
@@ -2535,6 +2630,7 @@ mod tests {
25352630 None ,
25362631 0 ,
25372632 f32:: INFINITY ,
2633+ & [ ] ,
25382634 )
25392635 } ;
25402636 let t0 = Instant :: now ( ) ;
@@ -2589,6 +2685,7 @@ mod tests {
25892685 None ,
25902686 0 ,
25912687 f32:: INFINITY ,
2688+ & [ ] ,
25922689 )
25932690 } ;
25942691
@@ -2641,6 +2738,7 @@ mod tests {
26412738 None ,
26422739 0 , // anchor at the top
26432740 viewport_h,
2741+ & [ ] ,
26442742 ) ;
26452743 let n = doc. line_count ( ) ;
26462744 assert ! ( n >= 3000 ) ;
@@ -2706,6 +2804,7 @@ mod tests {
27062804 None ,
27072805 anchor,
27082806 viewport_h,
2807+ & [ ] ,
27092808 ) ;
27102809 let n = doc. line_count ( ) ;
27112810 // Head virtualized: the band starts near the anchor, not at line 0.
@@ -2897,6 +2996,7 @@ mod tests {
28972996 None ,
28982997 0 ,
28992998 f32:: INFINITY ,
2999+ & [ ] ,
29003000 ) ;
29013001 // Table lines are the header/delimiter/body rows.
29023002 for ( line, want) in [
@@ -2960,6 +3060,7 @@ mod tests {
29603060 None ,
29613061 0 ,
29623062 f32:: INFINITY ,
3063+ & [ ] ,
29633064 )
29643065 } ;
29653066 // Cursor off the table (past the end): grid mode, header row hidden + flagged.
@@ -3006,6 +3107,7 @@ mod tests {
30063107 None ,
30073108 0 ,
30083109 f32:: INFINITY ,
3110+ & [ ] ,
30093111 )
30103112 }
30113113
0 commit comments