hue overlays — Feature Requirements (pluggable annotation overlays)
Status: planned (framework: researched) · Date: 2026-07-23 · Scope: the pluggable overlay layer of hue — a backend-agnostic seam that paints extra annotations over the highlighted source. Covers the framework and the registered overlay kinds. Twoslash is the first overlay of this kind and is specified in its own document; the others are specified here.
NOTE
Everything in this document is forward-looking design. The only overlay with an implementation is twoslash, and even that is branch-only (feat/syntax-twoslash, see twoslash.md). The framework requirements (OVL) are researched — the seam is proven by twoslash but not yet generalized in code; every non-twoslash overlay kind is not started (the tree-sitter inspector is researched, since it needs no external data — sparkles:syntax already builds the tree it reads). Status legend and ID conventions: see the overview.
Design & rationale
An overlay is a producer of decorations over hue's existing (source, highlight events) model; a backend-agnostic renderer paints those decorations. Twoslash proved the seam (twoslash.md § Architecture): a decoration is just an extra (start, length) push/pop pair fed alongside the highlight events (byStyledSpan already flattens overlapping ranges), plus below-line annotation blocks and hover popups — no per-overlay token-splitting engine, and the ANSI / HTML / GPU backends already know how to draw all of it.
The generalization: keep that decoration model and renderer contract, and make the producer and its data source the only things an overlay supplies. Twoslash's producer is a semantic backend answering the four-query contract; a coverage overlay's producer is an .lst/lcov parser; a tree-sitter inspector's producer is the parse tree itself. The renderer never learns which overlay it is drawing.
(source, highlight events, tree) ─┐
├─▶ OverlayProducer ─▶ OverlayModel ─▶ renderer (ANSI / HTML / GUI)
overlay-specific data artifact ─┘ (per kind) (uniform) (overlay-agnostic; = twoslash TWO*/backends)The overlay framework (OVL)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| OVL1 | A uniform OverlayModel must carry four decoration channels: inline span decorations (tint / underline / badge over a byte (start,length)), line/gutter decorations (per-line marker, tint, or count), below-line annotation blocks (meta-lines), and hover popups (rich content anchored at an offset). It generalizes twoslash's overlay plan (libs/twoslash overlay.d) to a shared, overlay-neutral shape. | researched/not-started | generalizes twoslash planTwoslash; proposed shared module |
| OVL2 | The renderers must be overlay-agnostic: ANSI, HTML, and the raylib GUI must paint any OverlayModel through the existing twoslash render primitives (TWO*, render_ansi/render_html). Adding an overlay adds a producer, never new render code. | partial | coverage paints across ANSI / HTML / GUI / TUI from one producer via the sparkles:ui display list (viewCodeDocument's gutter column) rather than the TWO* primitives; twoslash still has its own backends |
| OVL3 | Each overlay is a producer over (source, highlight events, tree) plus at most one overlay-specific data artifact (source-map / coverage / trace / size report). The tree-sitter inspector needs no external artifact; hue treats every other artifact as opaque input (as it already does the twoslash node JSON). | researched/not-started | proposed OverlayProducer seam |
| OVL4 | The CLI must select an overlay with --overlay <kind>[=<artifact>] (--list-overlays enumerates the registered kinds); an overlay must be available across the ANSI / HTML / GUI backends per the OVL2 contract, subject to each kind's own backend support notes. | partial | app.d --overlay/--list-overlays dispatch shipped with two registered kinds (twoslash, coverage); --overlay is a string[], so several are expressible, but the registry is still a hand-written switch rather than data-driven |
| OVL5 | Overlays must compose when their decoration channels don't collide (e.g. coverage's gutter + tracing's inline badges); a genuine channel conflict must be reported, not silently dropped. v1 may restrict to one overlay at a time and defer composition. | not started | app.d (proposed). The gutter half now has a shape to compose in: GUT* lets two overlays hold adjacent channels, and GUT7 bounds what that costs. It is not free, though — the note this row used to carry ("gutter composition is cheap, lanes are additive") was wrong for icons. A breakpoint, a diagnostic badge and a fold arrow want the same cell, and a lane each converts the collision into width; GUT6 resolves them by priority in one shared slot, which is a merge, not a composition. So OVL5's "genuine channel conflict" case is live in the gutter, not hypothetical |
| OVL6 | A missing or unparseable data artifact must warn and render the plain highlighted file (the totality law from the syntax spec / gui.md RND5); an overlay must never abort the render. | partial | shipped for coverage: every failure path warns and renders the plain file, which the ingest library's ParseExpected is what makes expressible; twoslash unchanged |
| OVL7 | The line/gutter decoration channel is new relative to twoslash (which uses inline + below-line only). It must share the GUI's existing gutter column region (gui.md NUM*) — coverage / size / tracing render a marker or count in the gutter next to (or in place of) the line number. | shipped | a GUT* GutterChannel in sparkles.ui.components.gutter. The requirement is met literally, but only after the sharing went the other way: rather than the overlay column joining the GUI's chrome gutter, the line numbers moved into the document tree and both are now channels of one model (85dcc4cec). An earlier revision of this row recorded the opposite as a deliberate deviation, on the grounds that the GUI's gutter was chrome painted outside the document and feeding 28 coordinate sites. That was true and it was the problem: it also meant the TUI had no line numbers at all, folds needed a pixel-range hit test, and every producer that prefixed chrome into a row displaced the decorations positioned by source column. Moving the chrome in cost far less than the audit suggested — gutterPx collapsed to the pane origin and the 28 sites were left untouched — and the TUI, which had never met NUM1, gained numbers and clickable fold arrows by construction. What remains open in the other direction is the ANSI and HTML static sinks, which still emit the coverage channel only |
| OVL8 | Overlay attachment must be per line-source, not per document: a diff pane shows two texts at once, so an overlay attaches to the old side, the new side, or both, and the renderer applies decorations against whichever text a row came from. This requires the per-line decoration application in render_widgets.d to be extracted from its payload-shaped entry point into a line-source-agnostic seam the diff's row builder can call — the structural prerequisite of the type overlay (DVT1), and the same seam that lets twoslash stop being a mode (OVL4). An overlay with no per-side notion attaches to the new side only. | partial (0dee5645) | proposed decoration seam; diff-view DVT1/DVT8 |
Registered overlays
The overlay registry. Twoslash is kind #1 (owned by its own doc); the rest are specified in the sections below.
| # | Kind | Area | Data source | Annotates | Status |
|---|---|---|---|---|---|
| 1 | twoslash | TWO / TWM | semantic backend (sparkles:dmd-lsp) or a TS-twoslash node JSON | inferred types, hovers, completions, errors, tags | planned/branch-only |
| 2 | source map | SMP | a Source Map v3 (.map) — alternative to twoslash | provenance: which original file/position a span maps to | not started |
| 3 | code coverage | COV | D -cov .lst listings, lcov .info | per-line/region hit counts (covered / uncovered) | not started |
| 4 | tracing / profiling | TRC | a trace/profile JSON in the sparkles:test-runner metric-catalog shape | per-function call count + wall-clock decomposition | not started |
| 5 | tree-sitter inspector | TSI | the tree-sitter parse tree itself (no external artifact) | node type / field / S-expression at the cursor | researched |
| 6 | function code size | CSZ | native symbol-size report (nm/bloaty/linker map) or a JS bundle report | bytes per function (.text segment, or minified size) | not started |
| 7 | diff decorations | diff-view DVM5 | the sparkles:diff decoration stream — a second span stream over both sides | added/removed/changed rows, intra-line segments, noise verdicts | future kind — diff ships standalone first, never gated on OVL |
Source-map overlay (SMP) — provenance, an alternative to twoslash
Where twoslash answers "what is the type here", the source-map overlay answers "where did this code come from" — it consumes a Source Map v3 and maps positions between a generated/minified artifact and its originals.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| SMP1 | The producer must parse a Source Map v3 (version, sources, sourcesContent, names, VLQ mappings) — resolved from --overlay source-map=<file.map> or an inline //# sourceMappingURL. | not started | proposed overlay/source_map.d |
| SMP2 | Over a shown generated file, each mapped region must be tinted by its originating source, with the original file:line:col (and mapped name) shown on hover (an inline + hover overlay). | not started | SMP producer → OverlayModel |
| SMP3 | Over a shown original file, the overlay must indicate which spans survive into the generated artifact and where (the inverse direction), and mark spans that were dropped. | not started | SMP producer (inverse index) |
Coverage overlay (COV)
Per-line / per-region execution coverage — the familiar green/red gutter.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| COV1 | The producer must ingest coverage data — D -cov .lst listings (leading per-line hit counts) and lcov .info — mapping hit counts to source lines/regions. | shipped | sparkles:code-instrumentation, beyond the two formats specced here: DMD .lst, gcov, LCOV .info, V8 block coverage and llvm-cov export JSON, over a shared record scanner |
| COV2 | Covered / uncovered / partial lines must render as a gutter tint (OVL7) plus an inline hit-count badge; branch or region partials are shown where the format carries them. | shipped | shipped: the gutter column carries a per-line background wash in the state's colour plus the hit count on it, and CodeViewOptions.tintedRanges washes the sub-line byte ranges a format carries — V8's block coverage today, which is what locates the dead part of a partial line. LineState.partial is produced by LCOV, gcov and V8 where they state branch or block outcomes. The count reads as a badge in the gutter rather than inline in the code: an inline badge per line is noise, and the sub-line tint is the inline channel that carries real information |
| COV3 | A file-level summary annotation must report the covered-line percentage (matching -cov's trailing summary line). | shipped | CoveragePlan.summaryBanner, surfaced as the viewer's summary line |
| COV4 | Opening a source file in a repository that has coverage data must light the overlay up without the reader naming an artifact. | shipped | ci --test collects coverage by default and merges every sub-package's listings into build/coverage; hue indexes that directory by each listing's trailer (the artifact name is lossy — / becomes -, and a package directory may contain a -) and matches the open file by longest whole-component path suffix. An artifact older than the file it describes is refused with a warning rather than painted, since its line numbers refer to text that has moved. --no-auto-cov opts out; naming an artifact still wins |
| COV5 | An artifact whose source has been edited since the run must be re-anchored onto the current text rather than refused: lines that survived the edit keep their counters at their new numbers, and only the lines that changed lose theirs. | shipped | A .lst records each counted line's source beside its counter, which is the evidence — coverage_rebase diffs it against the file on disk (sparkles:diff's Myers line diff) and re-numbers the survivors. Inserted or rewritten lines carry a LineCoverage.stale record and render ·, because suppressed must look different from absent. CoveragePlan.staleLines counts them and the banner says so. Only DMD listings carry the source; for every other format the rebase is the identity and the old freshness warning is still the only honest answer. Sub-line spans are dropped when a rebase happens — a line diff cannot move a byte offset |
Tracing overlay (TRC)
Per-function runtime cost, drawn from a profile. Its data model reuses the sparkles:test-runner metric catalog (SPEC § 5) — call count, a wall-clock decomposition into on-CPU vs attributable wait, average / total, allocations — so a trace and a --bench result render through the same Unit/Mode vocabulary.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| TRC1 | The producer must ingest a per-function (or per-line) trace: call count, wall-clock decomposition (on-CPU / attributable wait), average + total time, and allocations — the metric-catalog shape (docs/specs/test-runner/SPEC.md § 5). | not started | proposed overlay/tracing.d |
| TRC2 | Each function definition must carry an inline badge (×N, ~µs avg), heat-tinted by total time; a hover popup must show the full per-function metric breakdown, rendered through the catalog Unit/Mode formatting. | not started | TRC producer → inline + hover |
| TRC3 | The trace artifact (JSON, e.g. from a profiler or a sparkles:test-runner --workload/--bench-json run) is opaque input — hue maps its symbols/positions onto the source, as it does the twoslash node JSON. | not started | TRC producer; test-runner --bench-json |
Tree-sitter inspector overlay (TSI)
A debugging overlay — the tree-sitter-playground inspector, in hue. Unique among the overlays in needing no external artifact: it reads the parse tree that sparkles:syntax / sparkles:tree-sitter already build for highlighting.
NOTE
Delivered as a dock panel, not through the overlay framework: the TSI rows shipped as the inspector component's first hue mount (<leader>vi, both backends), matching the reference UX — neovim's :InspectTree is a split window. When the OVL* framework lands, the panel becomes its self-contained first migration candidate; nothing here blocks on it.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| TSI1 | The producer must read the tree-sitter tree directly (no data file); hovering (or moving the cursor onto) a token must show its node type, field name, and named-ancestor S-expression path. | full (8dc7128c) — hover drives the panel's selection while the ⌕ picker is armed (INS9; a click in the document pins the node); the row shows field: type, the tree shows the ancestor path, the details pane the rest | the retained parse (ViewerModel.ensureParsed); apps/hue/src/ts_inspect.d |
| TSI2 | The hovered node's byte extent must be outlined/tinted; a toggle must reveal anonymous nodes and mark ERROR/MISSING nodes distinctly. | full (8dc7128c) — extent tint via the viewer's identity channel (off-screen-only scroll-follow); a toggles anonymous with the cursor preserved; MISSING/ERROR take the error slot (exceeding the reference, which special-cases only MISSING) | ViewerModel.setInspectExtent; ts_inspect.d |
| TSI3 | A panel/annotation must render the S-expression for the current line or selection (the playground's tree view). | full (8dc7128c) — the panel IS the playground's tree view, and queryText emits the neovim-compatible query-syntax S-expression | inspector_pane.d; ts_inspect.queryText |
Function-code-size overlay (CSZ)
Per-function size — "how big did this compile to". Native and JS have different data sources, unified behind one overlay.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| CSZ1 | For native languages, the producer must map each function to its compiled .text segment size via a symbol-size report (nm / bloaty / linker map), keyed by symbol → source span. | not started | proposed overlay/code_size.d |
| CSZ2 | For JS/TS, the producer must map each function to its byte contribution in a minified bundle, via the bundle's source map (SMP1) plus a size report. | not started | CSZ producer (+ reuses SMP1) |
| CSZ3 | Each function definition must carry an inline byte badge, heat-tinted by size, with a per-function breakdown on hover and a file total; the ordering must make the largest functions findable. | not started | CSZ producer → inline + hover |
Milestones
The overlay framework is a design; there is no committed track yet. A sensible order once twoslash merges: O0 generalize twoslash's overlay plan into the shared OverlayModel + producer seam (OVL1–OVL3); O1 the --overlay dispatch + --list-overlays + degradation (OVL4/OVL6); O2 the two self-contained / file-free wins — the tree-sitter inspector (TSI, no data source) and coverage (COV, ubiquitous -cov/lcov formats); O3 source-map and code-size (SMP/CSZ, share the source-map parser); O4 tracing (TRC, once a trace artifact format is settled with sparkles:test-runner); O5 composition (OVL5).
Module coverage (overlays)
Proposed layout — no code on this branch yet; twoslash's overlay is the only existing instance (branch-only).
| Source (proposed / branch) | Requirements |
|---|---|
shared OverlayModel + producer seam (proposed) | OVL1–OVL3 |
apps/hue/src/app.d (--overlay dispatch, proposed) | OVL4, OVL5, OVL6 |
sparkles.ui.components.gutter (GUT*, spec) | OVL7 |
libs/twoslash overlay.d + backends (branch) | overlay #1 (→ twoslash.md) |
overlay/{source_map,coverage,tracing,code_size}.d (proposed) | SMP*, COV*, TRC*, CSZ* |
sparkles:tree-sitter / sparkles:syntax tree (existing) | TSI* |
→ Twoslash requirements · GUI requirements · General requirements · Overview