The previous fix still rendered gutter numbers misaligned with wrapped lines under wrap ON:
splitLines() joined .codeline blocks with a newline. Under white-space: pre-wrap those newline text nodes each render as an extra line box between blocks, shifting every subsequent number down one row per line.
renumber() built gutter rows by counting rows per .codeline height (round(h/lh)), but never accounted for those inter-line newline rows, and a trailing empty logical line (zero height) got no row at all — so numbers drifted by one row per wrapped line.
What changed (internal/web/static/paste-lines.js only)
splitLines() now joins the display:block .codeline spans with an empty string (no newline text nodes) so the code column and gutter share the same row grid.
renumber() (wrap ON) now assigns numbers by measured geometry: each number goes on the visual gutter row whose top matches its .codeline's measured top (round((lineTop - bodyTop) / lh)), with blank filler .gutline blocks elsewhere and the gutter sized to the deepest row any line starts on. No uniform row-count assumptions.
Wrap OFF path untouched: gutter is still 1\n2\n3\n4 text.
QA evidence (local build, headless chromium, CDP/Playwright DOM geometry)
4-line paste with line 3 = 800 unbroken chars, wrap ON:
1400x900: 5 .codeline, 14 .gutline rows, numbers 1..5, every number top matches its line top within lh/2 -> okAlign: true, no horizontal scroll, 0 console errors.
## Root cause
The previous fix still rendered gutter numbers misaligned with wrapped lines under wrap ON:
1. `splitLines()` joined `.codeline` blocks with a newline. Under `white-space: pre-wrap` those newline text nodes each render as an extra line box between blocks, shifting every subsequent number down one row per line.
2. `renumber()` built gutter rows by counting rows per `.codeline` height (`round(h/lh)`), but never accounted for those inter-line newline rows, and a trailing empty logical line (zero height) got no row at all — so numbers drifted by one row per wrapped line.
## What changed (internal/web/static/paste-lines.js only)
- `splitLines()` now joins the display:block `.codeline` spans with an empty string (no newline text nodes) so the code column and gutter share the same row grid.
- `renumber()` (wrap ON) now assigns numbers by measured geometry: each number goes on the visual gutter row whose top matches its `.codeline`'s measured top (`round((lineTop - bodyTop) / lh)`), with blank filler `.gutline` blocks elsewhere and the gutter sized to the deepest row any line starts on. No uniform row-count assumptions.
- Wrap OFF path untouched: gutter is still `1\n2\n3\n4` text.
## QA evidence (local build, headless chromium, CDP/Playwright DOM geometry)
4-line paste with line 3 = 800 unbroken chars, wrap ON:
- 1400x900: 5 `.codeline`, 14 `.gutline` rows, numbers 1..5, every number top matches its line top within lh/2 -> okAlign: true, no horizontal scroll, 0 console errors.
- 375x812 mobile: 26 rows, okAlign true, no horizontal scroll, 0 errors.
- 10-long-line paste (each wrapping to ~4 rows): 41 gutter rows, numbers 1-11, okAlign true.
- Live wrap toggle re-renumbers correctly (okAlign true after toggle).
- Wrap OFF verified unchanged: gutter textContent `1\n2\n3\n4\n5`, no `.gutline` spans.
Fixes #167
- splitLines(): join .codeline blocks with "" not "\n" — newline text
nodes under pre-wrap rendered an extra line box per logical line, so
every gutter number drifted one row per line
- renumber(): geometry-driven placement — one gutline per visual row of
the code body, each number assigned to the row matching its line's
measured offsetTop; verified second pass re-runs if a partially-filled
last wrapped row reflows after the gutter rebuild
- app.css: mobile media query gives .code .gutter the same 13px font as
.code so gutter rows and code rows share one line box at 375px
Verified in local build with headless chromium (long unbroken token on
line 5, wrap ON): all numbers align with line starts, no misalignment,
no horizontal scroll at 1400x900 and 375x812.
paste-lines.js joined .codeline spans with '\n' text nodes that pre-wrap
renders as extra line boxes, and renumber() counted per-line span heights
(rounding) instead of real geometry, so gutter numbers drifted off their
line starts on wrapped lines. Fix: join spans with '' (blocks need no
newline text), and build one .gutline per visual row measured from the
code body's real height, placing each number at the visual row whose top
matches its .codeline's measured top. Wrap OFF path unchanged. Mobile
font-size rule added so gutter row pitch matches code rows at 375x812.
Rebuild renumber() so gutter numbers are placed by measured geometry:
each .gutline block is one visual row and each number is assigned to the
row whose top matches its .codeline's top, instead of counting rows per
line. This keeps numbers aligned with wrapped line starts regardless of
how many visual rows a line occupies. Wrap OFF path unchanged.
fen
changed title from Fix #167: gutter numbers align with wrapped-line starts to Fix #167: gutter numbers align with actual rendered line rows (wrap ON) — final wrap-row accounting2026-09-10 17:13:14 +00:00
fen
changed title from Fix #167: gutter numbers align with actual rendered line rows (wrap ON) — final wrap-row accounting to Fix #167: gutter alignment with line wrapping enabled (geometry-driven renumber)2026-09-10 17:13:26 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Root cause
The previous fix still rendered gutter numbers misaligned with wrapped lines under wrap ON:
splitLines()joined.codelineblocks with a newline. Underwhite-space: pre-wrapthose newline text nodes each render as an extra line box between blocks, shifting every subsequent number down one row per line.renumber()built gutter rows by counting rows per.codelineheight (round(h/lh)), but never accounted for those inter-line newline rows, and a trailing empty logical line (zero height) got no row at all — so numbers drifted by one row per wrapped line.What changed (internal/web/static/paste-lines.js only)
splitLines()now joins the display:block.codelinespans with an empty string (no newline text nodes) so the code column and gutter share the same row grid.renumber()(wrap ON) now assigns numbers by measured geometry: each number goes on the visual gutter row whose top matches its.codeline's measured top (round((lineTop - bodyTop) / lh)), with blank filler.gutlineblocks elsewhere and the gutter sized to the deepest row any line starts on. No uniform row-count assumptions.1\n2\n3\n4text.QA evidence (local build, headless chromium, CDP/Playwright DOM geometry)
4-line paste with line 3 = 800 unbroken chars, wrap ON:
.codeline, 14.gutlinerows, numbers 1..5, every number top matches its line top within lh/2 -> okAlign: true, no horizontal scroll, 0 console errors.1\n2\n3\n4\n5, no.gutlinespans.Fixes #167
e430eb57b7to2149fcc2e1Fix #167: gutter numbers align with wrapped-line startsto Fix #167: gutter numbers align with actual rendered line rows (wrap ON) — final wrap-row accountingFix #167: gutter numbers align with actual rendered line rows (wrap ON) — final wrap-row accountingto Fix #167: gutter alignment with line wrapping enabled (geometry-driven renumber)Pull request closed