Skip to content

feat(web): focus the rendered markdown diff on changed blocks and list items - #629

Merged
danyaberezun merged 9 commits into
mainfrom
markdown-diff-focus
Oct 5, 2026
Merged

danyaberezun merged 9 commits into
mainfrom
markdown-diff-focus

Conversation

@danyaberezun

Copy link
Copy Markdown
Collaborator

Problem

Opening the Preview (rendered) diff of a markdown file showed the entire merged document with <ins>/<del> marks sprinkled through it. For a long document — this repo's own SPEC.md files, a README with forty sections — a one-paragraph edit meant scrolling the whole rendering to find it. The Source diff already collapses unchanged lines around changes (Pierre, git -U3 semantics); the rendered diff had no equivalent, so reviewers either hunted or switched to Source and lost the rendering.

Approach

Keep the worker-isolated htmldiff merge exactly as it is and change only what is shown afterwards: parse the merged HTML once, classify each top-level block as changed when it is or contains ins / del / [data-diff-node], and collapse runs of unchanged blocks with git hunk semantics — two blocks of context on each side of a change, leading/trailing runs keep context only where they touch a change, and a lone block is never hidden (an expander that replaces one paragraph saves nothing and costs a click). Each hidden run becomes one in-place expander bar: ⇕ 10 unchanged blocks · § Section 2, the trailing § naming the last heading hidden in the run — the section the visible content below belongs to, the markdown analogue of Pierre's line-info hunk separators.

Two decisions confirmed with the author before building:

  • Granularity: blocks and list items. The same rule recurses one level into a changed ul/ol (4 unchanged items), because a spec here routinely carries a thirty-bullet list with one edited bullet; collapsing only top-level blocks would still show the whole list. Ordered items keep their original number via value, so hiding items never renumbers the rest. Tables, quotes, and nested lists render whole (deferred).
  • No whole-document toggle. Expanders only; Source and the file preview already show the full document, and a Focus|Full toggle would add diff-pane state plus a renderer capability for a rarely needed mode.

Rejected alternatives: rendering source-line hunks as markdown fragments (a slice loses list/table/fence context and renders wrong), and hiding nodes with CSS after injecting the full HTML (fights React on every re-render). Visible blocks are re-created from the parsed elements (tag + attributes + innerHTML) rather than wrapped, so the DOM the tr-prose-doc descendant styles target is unchanged; boolean attributes (details[open]) are mapped explicitly because React drops an empty-string boolean.

A merge in which no block changed — front matter is stripped before rendering, whitespace and HTML comments don't render — now shows a rendered-diff-empty notice pointing at Source and collapses the document to a single expander, instead of presenting an unmarked full document as if it were a diff.

Changes

  • apps/web/src/panels/renderedDiffFocus.ts (new): focusSegments(items, isChanged, context) — the DOM-free collapsing rule (FOCUS_CONTEXT_BLOCKS = 2, minimum two hidden), generic over the item type; unit-tested in renderedDiffFocus.test.ts.
  • apps/web/src/panels/RenderedDiff.tsx: parses the merged HTML (DOMParser), renders the prose root through focusSegments, recurses into changed lists, emits rendered-diff-collapsed expander buttons (one-way, component-local positional state so a live refresh keeps an expansion whose run still starts at the same position), and the rendered-diff-empty notice. Loading/error placeholders, token marks, and scroll-state restore are unchanged.
  • apps/web/src/panels/SPEC.md: records the focus rule, expander semantics, the no-toggle decision, the empty case, and the re-create-not-wrap / boolean-attribute hazards in the RenderedDiff passage.
  • e2e/changes.spec.ts: two new tests — collapse + expand for blocks and list items (including a <details open> block surviving reconstruction), and the front-matter-only "preview is identical" case. Both use the Uncommitted scope, because "All changes vs main" treats a file committed only on the workspace branch as wholly added. The five existing rendered-diff tests needed no changes.

No contract, store, or renderer-capability changes; the markdown renderer registration (thinkrail/markdown) is untouched.

Screenshots

Same scenario (28-block doc, one paragraph and one bullet edited, Uncommitted scope), same viewport.

Before — as opened: the whole document from the top; the change is three sections down.

before, as opened

Before — after scrolling to the change:

before, scrolled to the change

After — as opened: the changed paragraph with two blocks of context on each side, hidden runs as expander bars naming their count and section; the edited bullet further down gets the same treatment inside its list.

after, as opened

Checklist

  • Fast gates pass: bun run lint → clean · bun run typecheck → 17/17 packages green via turbo run typecheck --filter='!@thinkrail/desktop'; the @thinkrail/desktop task could not complete locally because its electrobun prepare step deadlocks on a shared ~/.hutch lock in this environment (desktop is untouched by this PR — CI covers it) · bun run test → 18/18 tasks green (incl. the 6 new focusSegments tests)
  • E2E suite passes for app-affecting changes: bun run e2e → all 8 shards passed (467 tests); focused e2e/changes.spec.ts + e2e/live-refresh.spec.ts rendered-diff tests → 7 passed on the final head. Also run: bun run check:deps, check:boundaries, check:seams, check:spec-surface → OK; apps/web colour/spacing/typography usage guards → pass
  • Before/after screenshots are included for frontend changes, or marked not applicable
  • Relevant SPEC.md / top-level specs updated to reflect any boundary, contract, or behavior change
  • I have read the Contributing guide and agree to the Code of Conduct

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread apps/web/src/panels/RenderedDiff.tsx
Comment thread apps/web/src/panels/RenderedDiff.tsx
…licit li values in the focused markdown diff

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

…cal twin cannot mask an attribute-only change

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread apps/web/src/panels/RenderedDiff.tsx Outdated

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

Comment thread apps/web/src/panels/RenderedDiff.tsx Outdated

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit c55ca3c Oct 5, 2026
7 checks passed
@danyaberezun
danyaberezun deleted the markdown-diff-focus branch October 5, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants