Blob view: click/drag line-number selection with shareable #L links (single line and ranges) #243

Closed
opened 2026-09-09 17:19:08 +00:00 by crueber · 3 comments
Owner

What's requested

In the Code tab's blob view, clicking a line number should highlight that line and update the URL so the selection is shareable; clicking and dragging across multiple line numbers should highlight the whole range. This is the GitHub #L12 / #L12-L20 model.

Current state (code evidence)

  • The blob view renders the gutter and code as two separate <pre> elements (web/src/pages/Blob.jsx:139-146): the gutter is one <pre class="blob-gutter"> with all numbers joined by \n (:140-142), the code is another <pre class="code-view"> with a single highlighted <code innerHTML> (:143-145). There are no per-line elements at all — no ids, no per-line DOM nodes — so nothing is targetable by a fragment and there is nothing to click per line.
  • The gutter is user-select: none (web/css/repo.css:25) and both <pre>s are plain blocks — no click handlers exist in Blob.jsx (no signal for selection, no useSearchParams/hash read).
  • The route is /:owner/:name/blob/*rest (web/src/index.jsx:68); Blob.jsx already reads path/ref from useResolved + useRepo. No URL state for lines today.
  • Both .blob-gutter and .code-view set matching line-height: 1.45 / font metrics (repo.css:24-28), which is what keeps the two-column scroll aligned — any per-line restructure must preserve that pairing exactly or the columns desync.

Proposed design (for implementer)

  1. Per-line DOM. Render code view as per-line rows — either a single <table> (GitHub-style: line-number cell + code cell per row) or a flex column of paired cells. Keep the tokenizer output per line (lib/highlight.js already tokenizes; splitting highlighted HTML by line must respect multi-line tokens — verify the tokenizer never spans lines, or tokenize per line). Scroll behavior stays on the container (.blob-cols keeps overflow: auto).
  2. Selection model (Solid signals, no library):
    • mousedown on a line number sets the anchor line; mousemove over other numbers extends the focus end (drag); mouseup finalizes.
    • Plain click (no drag) = single line #L12.
    • Drag upward/downward normalizes to #Lstart-Lend.
    • Shift-click a second number extends the range from the existing anchor (cheap bonus, matches GitHub).
    • Clicking the code area (not the gutter) must not start selection — normal text selection in code stays intact.
  3. URL. Store selection in the hash: #L12 or #L12-L20 (GitHub-compatible). Use the router's hash handling or location.hash directly; replacing history state (not pushing) on each drag frame avoids history spam — push once on mouseup. On load, parse the hash and highlight + scroll the first selected line into view.
  4. Highlight styling. A .line-hl background rule (theme-aware, both light/dark — the repo uses zinc/emerald Tailwind tokens + var(--panel)/var(--bg) in repo.css); anchor end lines can get a stronger edge. Highlight must apply to both gutter number and code cell so the row reads as one unit.
  5. Scope. Code view only — the markdown Preview render (:130) is prose, not lines; GitHub does the same. The standalone raw link (:83) is unaffected.

Acceptance criteria

  • Clicking a line number highlights that line (gutter + code) and sets #L<n> in the URL; copying that URL and opening it re-renders with the line highlighted and scrolled into view.
  • Clicking one line number and dragging through others highlights the contiguous range and sets #L<start>-L<end>; dragging upward and downward both normalize to ascending order.
  • A pasted #L12-L20 URL highlights the range on load, including in a fresh tab (state comes from the URL, not session).
  • Normal text selection inside the code area still works; clicking/dragging in the code area does not alter the line selection.
  • The gutter/code columns remain pixel-aligned after the per-line restructure (same line-height, horizontal scroll still moves both columns together).
  • Works in light and dark themes.
  • Long files: scrolling to a distant #L target doesn't require loading anything extra (client-side only; no API change).
  • Headless unit test for the selection→hash string logic (pure function: anchor + focus + shift → #L/#L-L), per the settingsNav.js headless-test precedent.
## What's requested In the Code tab's blob view, clicking a line number should highlight that line and update the URL so the selection is shareable; clicking and dragging across multiple line numbers should highlight the whole range. This is the GitHub `#L12` / `#L12-L20` model. ## Current state (code evidence) - The blob view renders the gutter and code as **two separate `<pre>` elements** (`web/src/pages/Blob.jsx:139-146`): the gutter is one `<pre class="blob-gutter">` with all numbers joined by `\n` (:140-142), the code is another `<pre class="code-view">` with a single highlighted `<code innerHTML>` (:143-145). There are **no per-line elements at all** — no `id`s, no per-line DOM nodes — so nothing is targetable by a fragment and there is nothing to click per line. - The gutter is `user-select: none` (`web/css/repo.css:25`) and both `<pre>`s are plain blocks — no click handlers exist in `Blob.jsx` (no signal for selection, no `useSearchParams`/hash read). - The route is `/:owner/:name/blob/*rest` (`web/src/index.jsx:68`); `Blob.jsx` already reads path/ref from `useResolved` + `useRepo`. No URL state for lines today. - Both `.blob-gutter` and `.code-view` set matching `line-height: 1.45` / font metrics (`repo.css:24-28`), which is what keeps the two-column scroll aligned — any per-line restructure must preserve that pairing exactly or the columns desync. ## Proposed design (for implementer) 1. **Per-line DOM.** Render code view as per-line rows — either a single `<table>` (GitHub-style: line-number cell + code cell per row) or a flex column of paired cells. Keep the tokenizer output per line (`lib/highlight.js` already tokenizes; splitting highlighted HTML by line must respect multi-line tokens — verify the tokenizer never spans lines, or tokenize per line). Scroll behavior stays on the container (`.blob-cols` keeps `overflow: auto`). 2. **Selection model** (Solid signals, no library): - `mousedown` on a line number sets the anchor line; `mousemove` over other numbers extends the focus end (drag); `mouseup` finalizes. - Plain click (no drag) = single line `#L12`. - Drag upward/downward normalizes to `#Lstart-Lend`. - Shift-click a second number extends the range from the existing anchor (cheap bonus, matches GitHub). - Clicking the code area (not the gutter) must not start selection — normal text selection in code stays intact. 3. **URL.** Store selection in the hash: `#L12` or `#L12-L20` (GitHub-compatible). Use the router's hash handling or `location.hash` directly; replacing history state (not pushing) on each drag frame avoids history spam — push once on `mouseup`. On load, parse the hash and highlight + scroll the first selected line into view. 4. **Highlight styling.** A `.line-hl` background rule (theme-aware, both light/dark — the repo uses zinc/emerald Tailwind tokens + `var(--panel)`/`var(--bg)` in repo.css); anchor end lines can get a stronger edge. Highlight must apply to both gutter number and code cell so the row reads as one unit. 5. **Scope.** Code view only — the markdown **Preview** render (:130) is prose, not lines; GitHub does the same. The standalone raw link (:83) is unaffected. ## Acceptance criteria - [ ] Clicking a line number highlights that line (gutter + code) and sets `#L<n>` in the URL; copying that URL and opening it re-renders with the line highlighted and scrolled into view. - [ ] Clicking one line number and dragging through others highlights the contiguous range and sets `#L<start>-L<end>`; dragging upward and downward both normalize to ascending order. - [ ] A pasted `#L12-L20` URL highlights the range on load, including in a fresh tab (state comes from the URL, not session). - [ ] Normal text selection inside the code area still works; clicking/dragging in the code area does not alter the line selection. - [ ] The gutter/code columns remain pixel-aligned after the per-line restructure (same `line-height`, horizontal scroll still moves both columns together). - [ ] Works in light and dark themes. - [ ] Long files: scrolling to a distant `#L` target doesn't require loading anything extra (client-side only; no API change). - [ ] Headless unit test for the selection→hash string logic (pure function: anchor + focus + shift → `#L`/`#L-L`), per the settingsNav.js headless-test precedent.
Author
Owner

Fix is up: #256 (branch fix/issue-243, not merged). Per-line table code view with click/drag/shift gutter selection and shareable #L12/#L12-L20 URLs; per-line tokenization (block-comment spans can cross newlines, verified); both themes; keyboard accessible via native #L anchors. node suite 481/481 green, vite build verified. Browser interaction pass is open — shared-daemon guard blocks target attach/loopback.

Fix is up: #256 (branch fix/issue-243, not merged). Per-line table code view with click/drag/shift gutter selection and shareable #L12/#L12-L20 URLs; per-line tokenization (block-comment spans can cross newlines, verified); both themes; keyboard accessible via native #L anchors. node suite 481/481 green, vite build verified. Browser interaction pass is open — shared-daemon guard blocks target attach/loopback.
Author
Owner

Review of PR #256 (fix/issue-243, blob line selection):

VERIFIED (scratch worktree /tmp/pr256, since removed):

  • Single-table pairing: old two-
     (blob-gutter) fully replaced by one  with gutter+code in the same  (Blob.jsx:141,154-166). No blob-gutter leftover; remaining code-view 
    s are other pages + the .md Code-tab fallback (Blob.jsx:251), not the replaced layout.
    
  • Per-line ids stable:
  • with n=i()+1 1-based, gutter renders the same n (Blob.jsx:145,154,164). Past-EOF hashes match no row and scroll nowhere (Blob.jsx:88).
  • Selection model walked: mousedown sets anchor + replaceState preview, mouseover extends (normalized ascending via dragRange/rangeOf), mouseup ends drag, click pushes ONE entry via location.hash; hashchange re-highlights with sameSelection echo guard (Blob.jsx:60-137). replaceState during drag + single push on click confirmed; no history spam, no echo loop.
  • Text selection undisturbed: handlers only on gutter (mousedown/mouseover/click); code
  • cells handler-free (Blob.jsx:156-168).
  • Tokenizer-per-line claim verified by execution: highlight('/* foo\nbar /','js') wraps the newline inside one (highlight.js:65 [\s\S]?), so whole-text-then-split would break tags; per-line avoids it and unclosed /* renders plain. Correct.
  • Highlight styling both themes: .blob-row.line-hl unlayered amber #fef3c7 light / rgba(245,158,11,.16) dark + emerald inset gutter edge (ui.css). Emerald #10b981 visible on both.
  • MD preview out of scope correctly: MarkdownBlob path untouched; .md Code-tab fallback still whole-text (Blob.jsx:251) — noted below, non-blocking.
  • No new deps: package.json untouched; imports are solid-js/router/solid only. Laws 1/7/8/12 hold (doc §2.7 updated in same change).
  • Tests: blob-lines.test.js 10/10 pass; full node suite 481 pass / 0 fail (smoke.test.js cancelled — port 8080 held by unrelated listener in this env; identical hang on main, pre-existing, not PR-caused). vite build clean (135 modules).
  • FIXED + PUSHED to origin/fix/issue-243 (08a3d9f, re-tested 10/10 + vite clean):

    1. Blob.jsx onNumClick — plain keyboard Enter (detail 0, no shift) extended from the stale anchor instead of jumping to the focused line (deviated from native href + GitHub parity). Now: plain Enter selects the focused line and resets anchor; Shift+Enter extends. Pin added to blob-lines.test.js.
    2. Blob.jsx — dragging stayed true if the window lost focus mid-drag (Alt-Tab), so the next gutter hover would rewrite selection+URL. Added window blur -> endDrag.

    NON-BLOCKING NOTES:

    • .md Code-tab fallback has no #L selection (whole-text
      ); fine as follow-up if wanted.
    • inSelection() helper is tested but Blob.jsx inlines its own equivalent; harmless duplication.

    RECOMMENDATION: ready to merge (after author confirms the real-Chromium pass already noted open in the doc).

Review of PR #256 (fix/issue-243, blob line selection): VERIFIED (scratch worktree /tmp/pr256, since removed): - Single-table pairing: old two-<pre> (blob-gutter) fully replaced by one <table class=blob-table> with gutter+code in the same <tr> (Blob.jsx:141,154-166). No blob-gutter leftover; remaining code-view <pre>s are other pages + the .md Code-tab fallback (Blob.jsx:251), not the replaced layout. - Per-line ids stable: <tr id=L${n}> with n=i()+1 1-based, gutter renders the same n (Blob.jsx:145,154,164). Past-EOF hashes match no row and scroll nowhere (Blob.jsx:88). - Selection model walked: mousedown sets anchor + replaceState preview, mouseover extends (normalized ascending via dragRange/rangeOf), mouseup ends drag, click pushes ONE entry via location.hash; hashchange re-highlights with sameSelection echo guard (Blob.jsx:60-137). replaceState during drag + single push on click confirmed; no history spam, no echo loop. - Text selection undisturbed: handlers only on gutter <a> (mousedown/mouseover/click); code <td> cells handler-free (Blob.jsx:156-168). - Tokenizer-per-line claim verified by execution: highlight('/* foo\nbar */','js') wraps the newline inside one <span> (highlight.js:65 [\s\S]*?), so whole-text-then-split would break tags; per-line avoids it and unclosed /* renders plain. Correct. - Highlight styling both themes: .blob-row.line-hl unlayered amber #fef3c7 light / rgba(245,158,11,.16) dark + emerald inset gutter edge (ui.css). Emerald #10b981 visible on both. - MD preview out of scope correctly: MarkdownBlob path untouched; .md Code-tab fallback still whole-text (Blob.jsx:251) — noted below, non-blocking. - No new deps: package.json untouched; imports are solid-js/router/solid only. Laws 1/7/8/12 hold (doc §2.7 updated in same change). - Tests: blob-lines.test.js 10/10 pass; full node suite 481 pass / 0 fail (smoke.test.js cancelled — port 8080 held by unrelated listener in this env; identical hang on main, pre-existing, not PR-caused). vite build clean (135 modules). FIXED + PUSHED to origin/fix/issue-243 (08a3d9f, re-tested 10/10 + vite clean): 1. Blob.jsx onNumClick — plain keyboard Enter (detail 0, no shift) extended from the stale anchor instead of jumping to the focused line (deviated from native href + GitHub parity). Now: plain Enter selects the focused line and resets anchor; Shift+Enter extends. Pin added to blob-lines.test.js. 2. Blob.jsx — dragging stayed true if the window lost focus mid-drag (Alt-Tab), so the next gutter hover would rewrite selection+URL. Added window blur -> endDrag. NON-BLOCKING NOTES: - .md Code-tab fallback has no #L selection (whole-text <pre>); fine as follow-up if wanted. - inSelection() helper is tested but Blob.jsx inlines its own equivalent; harmless duplication. RECOMMENDATION: ready to merge (after author confirms the real-Chromium pass already noted open in the doc).
Author
Owner

Fixed by PR #256 incl. review keyboard/blur-drag fixes (single-table pairing, shareable #L links; 481/481), merged. Closing.

Fixed by PR #256 incl. review keyboard/blur-drag fixes (single-table pairing, shareable #L links; 481/481), merged. Closing.
crueber added this to the v1 milestone 2026-09-10 22:27:10 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#243
No description provided.