Skip to content

Diff viewer: copyable line-anchor links #126

Description

@gregoryfoster

Dependencies rewritten 2026-08-19 in a backlog staleness pass. The design and acceptance criteria are unchanged and still sound; only the blocker status was wrong.

Background

Split out from the descoped #115 Phase D. Phase A + B.1 of the diff viewer have shipped; this is the remaining "polish" item that's still relevant.

Problem

When a Watcher user wants to share a specific change with someone (a colleague, a stakeholder, a comment in another tool), they currently have to send the whole /changes/{id} URL — there's no way to point at a specific line within the diff. For long diffs that means the recipient has to scroll-hunt for what's being referenced.

Goal

Each line in the diff (and ranges) gets a stable, copyable URL fragment, e.g.:

  • https://watcher.exe.xyz/changes/{id}#L42 — single line
  • https://watcher.exe.xyz/changes/{id}#L42-L57 — range

When the URL with a fragment is loaded, the page should:

  • Auto-scroll to the referenced line(s)
  • Visually highlight the referenced span (subtle background tint)

Approach sketch

diff2html-ui renders each line as .d2h-code-line / .d2h-code-side-line with an associated line number cell. Two pieces:

  1. Anchor IDs at render time (src/dashboard/static/js/diff-viewer.js): after Diff2HtmlUI.draw(), walk the rendered DOM and add id="L{n}" to each line number cell. Add a small "copy link" affordance on hover (chip near the line number that copies ${location.pathname}${location.search}#L{n} to clipboard).
  2. Range selection — shift-click a second line number to extend a selection; #L{a}-L{b} form. Standard pattern from GitHub.
  3. Fragment handling on load — read location.hash, scroll the line into view, apply a .d2h-line-anchor-target class for highlight.

Acceptance

  • Each diff line has a stable id="L{n}" after Diff2HtmlUI.draw()
  • Visiting /changes/{id}#L42 scrolls to line 42 and visually highlights it
  • Visiting /changes/{id}#L42-L57 highlights the range
  • A copy-link affordance is reachable via keyboard (a11y)
  • HTMX mode swaps (Extracted ↔ Raw) re-attach IDs after re-render
  • Theme toggle preserves anchor highlight
  • Works in both outputFormat: "side-by-side" and "line-by-line"

Out of scope

  • Cross-mode anchor preservation (line 42 in Extracted ≠ line 42 in Raw — that's a different feature)
  • Persisting selections in localStorage

Dependencies

Blocked by #222. The original said "None — purely client-side enhancement on top of existing diff2html-ui mount." That is now backwards — there is no mount. Verified 2026-08-19:

Prerequisite State
/changes/{id} route Gone — no changes route in src/dashboard/routes/
.diff-mount / data-unified-diff / #diff-content in templates Zero occurrences
Any template loading diff-viewer.js None
Diff2HtmlUI.draw() ever running Never — nothing mounts it

Every acceptance criterion below targets DOM that is not rendered. The chain is:

#126 (this)  →  #222 (restore unified diff + chunk semantics)  →  a persistent content store

ChangeRevision stores a fingerprint and nothing else, so there is no previous content to diff and therefore no diff page to anchor into.

Note also #273 — the orphaned viewer assets (diff2html-ui.min.js, diff-viewer.js, the input.css .diff-mount overrides) are proposed for deletion, recoverable from 89d64eb. If that lands first, this issue's approach sketch needs re-vendoring diff2html as a step, and whoever restores it should confirm diff2html is still the right choice rather than restoring 3.4.52 reflexively.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions