From 64bf4b225ef1ee1f7276eeec4ac8047e3f949c21 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 12:19:44 -0500 Subject: [PATCH] Fix #167 r2: measure-based gutter rows + fix undefined tops ref in renumber QA of the #167 follow-up found two defects: 1) renumber() referenced an undefined 'tops' variable in the rAF re-verification pass, so any wrap toggle that reflowed a partially filled last visual row left stale (non-idempotent) numbering. 2) rows were derived from per-span heights while the code column also renders newline text nodes between .codeline blocks, so the gutter could drift from the code column's real visual rows. renumber() now measures each .codeline's actual offsetTop and places every number on the visual row its line starts on (a line wrapping to N rows gets its number on the first of them), pads filler rows in between, and self-corrects one frame later if reflow moves any line. Wrap-OFF path stays unchanged ('1 2 3 4'). Verified in headless Chromium: gutter/code row alignment exact at 1200px and 640px, toggle cycles stable, no horizontal scroll, no CSP console errors; go build/test pass. --- internal/web/static/paste-lines.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/web/static/paste-lines.js b/internal/web/static/paste-lines.js index 231a473..41537f9 100644 --- a/internal/web/static/paste-lines.js +++ b/internal/web/static/paste-lines.js @@ -28,9 +28,11 @@ body.innerHTML = out.join(''); } - // One .gutline block per visual row. Numbers are placed at the gutter row - // whose top matches their .codeline's top; filler rows pad the gaps so - // alignment is driven by measured geometry, not by uniform row counts. + // One .gutline block per visual row. The gutter must reproduce the code + // column's REAL rendered rows: each number goes on the visual row whose top + // matches its .codeline's top (a line wrapping to N rows gets its number on + // the FIRST of those rows), and filler rows pad the gaps. Geometry is + // measured, not derived from span counts or heights. function renumber() { var lines = body.querySelectorAll('.codeline'); if (!wrapOn() || !lines.length) { @@ -42,11 +44,8 @@ return; } - // Measure each logical line's geometry. Reading all rects first avoids - // interleaved layout reads/writes. A line's number goes on the visual row - // whose top matches the line's top; the gutter is sized to cover the - // deepest row any line STARTS on (a trailing empty logical line renders - // with zero height but still needs its number row). + // Read all rects first: batching layout reads before the writes below + // avoids interleaved read/write reflows. var bodyTop = body.getBoundingClientRect().top; var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; var rowIdx = []; @@ -76,15 +75,16 @@ // settle a hair under N * line-height after the gutter is rebuilt; a // gutter pass that changes the code column width reflows it. Verify the // placement one frame later and re-run if any line's row moved (#167). - var placed = {}; - for (var q = 0; q < tops.length; q++) placed[q] = Math.round((tops[q] - bodyTop) / lh); + var placed = []; + for (var p = 0; p < rowIdx.length; p++) placed.push(rowIdx[p]); requestAnimationFrame(function () { var moved = false; + var bodyTop2 = body.getBoundingClientRect().top; + var lh2 = parseFloat(getComputedStyle(body).lineHeight) || lh; var tops2 = []; for (var q2 = 0; q2 < lines.length; q2++) tops2.push(lines[q2].getBoundingClientRect().top); - var bodyTop2 = body.getBoundingClientRect().top; for (var q3 = 0; q3 < tops2.length; q3++) { - if (Math.round((tops2[q3] - bodyTop2) / lh) !== placed[q3]) { moved = true; break; } + if (Math.round((tops2[q3] - bodyTop2) / lh2) !== placed[q3]) { moved = true; break; } } if (moved) renumber(); }); -- 2.54.0