From e430eb57b7adbabcf5ee4571fb56af35c81cd1b6 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 11:50:00 -0500 Subject: [PATCH] Fix #167: renumber gutter to visual rows when line wrap is on Split the paste codebody into per-line block spans (paste-lines.js) and rebuild the gutter from each line's wrapped height so numbers count visual rows, not logical lines. Pinned the gutter width (flex 0 0 auto, 3ch) so its row count no longer feeds back into the code column width. ResizeObserver renumbers on wrap toggle / resize; wrap-off behavior is unchanged (one number per newline). --- internal/web/static/app.css | 13 +++++++++++++ internal/web/static/paste-lines.js | 22 +++++++--------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 463e78a..b158596 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -279,6 +279,10 @@ body { .btn-icon.wrap-toggle[aria-pressed="true"] { background: var(--accent); color: var(--bg); border-color: var(--accent); } /* #152: wrap ON must break long unbroken tokens mid-word and allow no horizontal scrolling */ html[data-wrap] .codebody { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all; overflow-x: hidden; } +/* #167: overflow:hidden zeroes the flex auto-minimum, so the codebody must + be told to fill the space left of the gutter or it collapses and the + gutter (flex-shrink:0) consumes the whole row */ +html[data-wrap] .code .codebody { flex: 1 1 auto; min-width: 0; } html[data-wrap] .editor { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all; overflow-x: hidden; } /* #152: with wrap on nothing may scroll horizontally, including the code container and mobile floats */ html[data-wrap] .code { overflow-x: hidden; } @@ -293,6 +297,12 @@ html[data-wrap] .float { overflow-x: hidden; } font-family: var(--font-mono); font-size: var(--code-fs); line-height: var(--code-lh); padding: 14px 0; display: flex; overflow-x: auto; } +/* #167: the gutter must not drive the flex layout — its content width + (row count × number width) shrinks the code column, which re-wraps lines, + which grows the gutter: a feedback loop. Pin the gutter with a fixed + basis, take its width out of the negotiation, and let the codebody take + the rest. */ +.code .gutter { flex: 0 0 auto; width: 3ch; min-width: 3ch; overflow: visible; } .code .gutter { flex-shrink: 0; } /* gutter/code share line metrics; the editor gutter keeps its own padding (#50) */ .code .gutter { padding-top: 0; padding-bottom: 0; } @@ -811,3 +821,6 @@ button[type="submit"]:focus-visible, .mt18 { margin-top: 18px; } .toggle-inline { display: inline-flex; } .wrap-normal { word-break: normal; overflow-wrap: break-word; } +.created-banner { display: block; } +/* #167: gutter rows for wrapped paste view — one row per visual code line */ +.gutline { display: block; } diff --git a/internal/web/static/paste-lines.js b/internal/web/static/paste-lines.js index 0eac3d3..048e3b5 100644 --- a/internal/web/static/paste-lines.js +++ b/internal/web/static/paste-lines.js @@ -1,7 +1,7 @@ // #167: with line wrapping on, a logical line can occupy several visual // lines; the gutter must show one number per VISUAL line. Per-line spans -// give each logical line its own box so offsetTop order stays correct even -// when highlighting spans cross no line boundaries. +// give each logical line its own box so its height identifies the wrapped +// row count; blank gutter rows keep numbers aligned with line starts. (function () { var body = document.getElementById('codebody'); var gutter = document.getElementById('gutter'); @@ -31,9 +31,9 @@ // Each logical line block occupies rows = height / line-height when // wrapped; its number sits on the first row and the remaining rows get // blank gutter lines so numbers stay aligned with line starts. - // The code column width must not change while measuring (the gutter is - // flex-shrink:0 so its own row count never affects it), and renumber - // must be idempotent to avoid a ResizeObserver feedback loop. + // The gutter has a fixed CSS width now (.code .gutter), so its row + // count never changes the code column width: measurement is stable + // with no measurement/layout feedback. var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; gutter.textContent = ''; var frag = document.createDocumentFragment(); @@ -61,19 +61,11 @@ renumber(); // Re-renumber on toggle (theme.js toggles data-wrap on ) and on any - // size change (paste of lots of text, window resize, zoom). Only width - // changes of the code column affect wrapping, so observe width only — - // height changes caused by our own renumbering must not re-trigger. + // size change (paste of lots of text, window resize, zoom). var mo = new MutationObserver(renumber); mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-wrap'] }); - var lastW = body.getBoundingClientRect().width; if (window.ResizeObserver) { - var ro = new ResizeObserver(function () { - var w = body.getBoundingClientRect().width; - if (Math.abs(w - lastW) < 0.5) return; - lastW = w; - renumber(); - }); + var ro = new ResizeObserver(function () { renumber(); }); ro.observe(body); } else { window.addEventListener('resize', renumber);