fix(scroll-sync): measure a block from the scroll container, not from its offset parent - #515
Merged
Merged
Conversation
…g imports Four functions in MarkdownViewer.svelte have no caller. Three of them are stale copies of render steps that moved elsewhere and kept evolving there: processTaskItems and processBlockIds moved into markdown.ts in 09fa88f, and the ==highlight== rewrite moved into the Rust renderer, where it later grew code-span protection (#228, #371). Each migration re-pointed the call site and left the old body behind, so the copies have been sitting there since b46a283 looking like reusable helpers. getSplitTransition is plain dead. Also removed: 24 exports nothing outside their own file imports, the escapeHtmlText alias that only forwards to escapeHtml, an addFrontMatterList- Item wrapper with no production caller, the toPlainRecord guard that isFrontMatterMapping already makes unreachable, and FrontMatterField's editableValue, which computes the same string as displayValue in every branch with nothing to keep the two from drifting apart. No behaviour change. The front matter tag test now calls the plural helper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same sweep as the previous commit, finished in scripts/. Twenty-two names in keymapHarness.ts, renderProtocolDom.ts, renderProtocolFixtures.ts, windowTagEditor.ts and scrollSyncBlockMapping.test.ts are only used inside their own file, so the export widened the contract for nothing. RenderFixtureName had no use anywhere, including its own file, and is gone. tsconfig.json puts scripts/ inside `npm run check`, so a name another file still imports would fail the type check rather than pass silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… not from its offset parent The per-block mapping resolves a position by descending to the annotated element that owns it and reading `offsetTop`. `offsetTop` is measured from the element's OFFSET PARENT, which CSSOM defines as the nearest positioned ancestor -- or the nearest `table`, `td` or `th`, positioned or not. The preview contains both kinds, and comrak stamps a source range on every table row and cell, so the descent goes inside a table and reads table-relative offsets as document offsets. Measured in Chrome over this app's own stylesheet: a row 945px down a document reports `offsetTop = 1`, and a `<pre>` 1,085px down reports 0 because `renderRichContent` wraps every code block in a `position: relative` shell for the copy button. Both halves of #205's surviving report follow. A line inside a table maps to a couple of hundred pixels, so the pane jumps to the top of the document; and every offset in the table is far below every row's box, so the reverse direction sticks on the last row until the reader is past the whole table. `measureAnchorBox` sums the offset-parent chain and subtracts the container's own. Subtracting rather than stopping at the container keeps it exact whether or not the container is itself an offset parent. A rendered Mermaid diagram now also inherits the source range of the `<pre>` it replaces. Mermaid's SVG keeps nothing of the code block, so a diagram was the one block in the preview that mapped to no source line at all -- not even through a descendant -- and several hundred pixels of preview were attributed to whatever block was nearest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…set-parent # Conflicts: # src/lib/utils/previewAnchor.ts
PathGao
added a commit
that referenced
this pull request
Aug 7, 2026
3,164 lines: 124 headings, 543 table rows, three Mermaid diagrams, code blocks in eight languages, footnotes, math, raw HTML and text in seven scripts. Written to compare Markdown readers, and the document that found both defects fixed in #515 — a table's rows and a rendered diagram report positions the scroll mapping could not read. Authored to @Guardiancelte, who wrote it, reported #205 and retested every round of scroll-sync fixes against it. Co-authored-by: Guardiancelte <174342084+Guardiancelte@users.noreply.github.com>
PathGao
pushed a commit
that referenced
this pull request
Aug 7, 2026
3,164 lines: 124 headings, 543 table rows, three Mermaid diagrams, code blocks in eight languages, footnotes, math, raw HTML and text in seven scripts. Written to compare Markdown readers, and the document that found both defects fixed in #515 — a table's rows and a rendered diagram report positions the scroll mapping could not read. Authored to @Guardiancelte, who wrote it, reported #205 and retested every round of scroll-sync fixes against it.
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows #474, on @Guardiancelte's retest of it: the per-block mapping is right, the ruler it reads is not.
offsetTopis not the distance to the top of the documentThe mapping resolves a position by descending to the annotated element that owns it and reading
offsetTop. That is measured from the element's offset parent, which CSSOM defines as the nearest positioned ancestor — or the nearesttable,tdorth, positioned or not. #474 assumed one offset parent for everything in the preview and said so in a comment. The preview has plenty.Measured in Chrome, over this app's own
styles.css:comrak stamps a source range on every table row and cell, so the descent goes inside a table and reads that middle column as if it were the right-hand one.
renderRichContentwraps every code block in aposition: relativeshell for the copy button, so a code block reports0.Both of @Guardiancelte's cases are that, from the two directions:
The direction that misbehaves depends on which pane is sending, which is why it looked like the mouse side mattered.
The fix
measureAnchorBoxsums the offset-parent chain and subtracts the container's own sum. Subtracting rather than stopping at the container is what keeps it exact whether or not the container is itself an offset parent — with.markdown-body'stransform: translate3d(0,0,0)Chrome makes it one, but nothing should depend on that.The tab-restore path and the front-matter carve-out read the same measurement now; both used raw
offsetTopand had the same defect (an anchor saved inside a table reopened the tab at the top).Mermaid — the other half of the report
preEl.replaceWith(container)gave the diagram none of the code block'sdata-sourcepos, and Mermaid's SVG keeps nothing of the<pre>either, so a diagram was the one block in the preview that mapped to no source line at all — not even through a descendant. Several hundred pixels of preview got attributed to whatever block was nearest, which is why the two panes' top headings disagreed by about a diagram (# 11. Mermaid DiagramsagainstSequence Diagram). The container now inherits the range it replaced.Cost
Two to three extra
offsetTopreads per candidate. Measuring every annotated element of a 9,900-element preview in Chrome: 2.20 ms read raw, 6.30 ms walked. A descent measures a few dozen, so this is tens of microseconds on a scroll event, against the 0.89 ms the descent itself costs on a 13,356-line document.Tests
scripts/scrollSyncBlockMapping.test.tshad the blind spot in the shape of its own layout:layOuthanded the mapping one flat stack of absolute tops, which models a preview containing neither a table nor a positioned wrapper. Same class of miss as #464 — the shim has no layout, so what it does not model cannot fail.It now models the offset parent instead. Every box is converted to what a browser would report before the production
measureAnchorBoxconverts it back, the fixture's tables carry the row and cell ranges comrak really emits, andwrapCodeBlocksputs the.code-block-shellaround the<pre>s the wayrenderRichContentdoes.rawMeasure—offsetTopread raw — is kept alongside as the control, the wayRATIO_ONLYis.Falsification, mapping reverted to raw
offsetTop, tests kept:That third one is the reporter's screenshots exactly: one scroll click down, and the other pane at 34px.
And with only the Mermaid line reverted:
Verification
npm run check(0 errors),npm test(782 passing),cargo test(125 passing).npm auditreports one moderate advisory in mermaid 11.16.0, unrelated to this change and present onmaster.What is still modelled rather than measured is the layout — whether a browser lays a table out the way the fixture's heights say. What is now measured is the thing that broke: the numbers in the table at the top of this description are from Chrome, not from the shim.
@Guardiancelte — thank you, both of those cases were real and neither was the mapping I described to you; they were the measurement under it. Your large-table file is the one I would most like retested once this lands. The remaining difference you may still notice is the one named in #474: what the panes align is the line at the top of each viewport, so at the very end of a document the preview is not necessarily at its own last pixel.