Skip to content

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)

IDRequirementStatusTraces to
OVL1A 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-startedgeneralizes twoslash planTwoslash; proposed shared module
OVL2The 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.partialcoverage 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
OVL3Each 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-startedproposed OverlayProducer seam
OVL4The 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.partialapp.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
OVL5Overlays 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 startedapp.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
OVL6A 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.partialshipped for coverage: every failure path warns and renders the plain file, which the ingest library's ParseExpected is what makes expressible; twoslash unchanged
OVL7The 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.shippeda 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
OVL8Overlay 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.

#KindAreaData sourceAnnotatesStatus
1twoslashTWO / TWMsemantic backend (sparkles:dmd-lsp) or a TS-twoslash node JSONinferred types, hovers, completions, errors, tagsplanned/branch-only
2source mapSMPa Source Map v3 (.map) — alternative to twoslashprovenance: which original file/position a span maps tonot started
3code coverageCOVD -cov .lst listings, lcov .infoper-line/region hit counts (covered / uncovered)not started
4tracing / profilingTRCa trace/profile JSON in the sparkles:test-runner metric-catalog shapeper-function call count + wall-clock decompositionnot started
5tree-sitter inspectorTSIthe tree-sitter parse tree itself (no external artifact)node type / field / S-expression at the cursorresearched
6function code sizeCSZnative symbol-size report (nm/bloaty/linker map) or a JS bundle reportbytes per function (.text segment, or minified size)not started
7diff decorationsdiff-view DVM5the sparkles:diff decoration stream — a second span stream over both sidesadded/removed/changed rows, intra-line segments, noise verdictsfuture 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.

IDRequirementStatusTraces to
SMP1The 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 startedproposed overlay/source_map.d
SMP2Over 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 startedSMP producer → OverlayModel
SMP3Over 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 startedSMP producer (inverse index)

Coverage overlay (COV)

Per-line / per-region execution coverage — the familiar green/red gutter.

IDRequirementStatusTraces to
COV1The producer must ingest coverage data — D -cov .lst listings (leading per-line hit counts) and lcov .info — mapping hit counts to source lines/regions.shippedsparkles: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
COV2Covered / 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.shippedshipped: 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
COV3A file-level summary annotation must report the covered-line percentage (matching -cov's trailing summary line).shippedCoveragePlan.summaryBanner, surfaced as the viewer's summary line
COV4Opening a source file in a repository that has coverage data must light the overlay up without the reader naming an artifact.shippedci --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
COV5An 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.shippedA .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.

IDRequirementStatusTraces to
TRC1The 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 startedproposed overlay/tracing.d
TRC2Each 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 startedTRC producer → inline + hover
TRC3The 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 startedTRC 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.

IDRequirementStatusTraces to
TSI1The 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 restthe retained parse (ViewerModel.ensureParsed); apps/hue/src/ts_inspect.d
TSI2The 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
TSI3A 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-expressioninspector_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.

IDRequirementStatusTraces to
CSZ1For 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 startedproposed overlay/code_size.d
CSZ2For 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 startedCSZ producer (+ reuses SMP1)
CSZ3Each 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 startedCSZ 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 (OVL1OVL3); 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)OVL1OVL3
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