[diffs] Add consumer span decorations for sub-line range styling#810
[diffs] Add consumer span decorations for sub-line range styling#810chsmc-ant wants to merge 5 commits into
Conversation
Add a public spanDecorations input that lets consumers style arbitrary character ranges within a line, using the same shiki decorations pipeline that powers the internal intra-line word/char diff highlight. A SpanDecoration addresses a range with a 1-based lineNumber plus spanStart/spanLength character offsets and resolves to a className on the wrapping span (plus a data-span-decoration attribute). The diff variant, DiffSpanDecoration, adds the same side field used by DiffLineAnnotation to target the deletions or additions side. Only structured input is accepted; classes are the styling contract, in line with the package's existing unsafeCSS posture. Decorations flow like lineAnnotations: render/hydrate props on File, FileDiff and UnresolvedFile, a spanDecorations field on CodeView items, props on the React wrappers, and options on the SSR preload helpers. Renderers translate file line numbers into bucket-local shiki DecorationItems, clamp ranges to the rendered line length, and drop out-of-range spans. Consumer spans are pushed after the internal diff spans so shiki nests them when ranges overlap and both stay applied. Because shiki bakes decorations into the highlighted AST, they are included in Render*Options and the equality helpers via the new areSpanDecorationsEqual, so cached or worker-produced ASTs that were highlighted without them are invalidated and re-highlighted locally.
|
@chsmc-ant is attempting to deploy a commit to the Pierre Computer Company Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 051df499c0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| continue; | ||
| } | ||
| const lineLength = cleanLastNewline(lineContent).length; | ||
| const spanStart = Math.min(decoration.spanStart, lineLength); |
There was a problem hiding this comment.
Clamp negative span starts before decorating
When a consumer passes a negative spanStart (for example from indexOf returning -1), this only caps the upper bound and forwards the negative character offset to Shiki; Shiki treats negative character positions as offsets from the end of the line, so an invalid/out-of-range decoration can wrap the wrong text instead of being dropped as this API documents. Please clamp the lower bound to 0 or reject these spans before calling createSpanDecoration (and mirror the fix in the diff helper).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 96f28de. Negative spanStart values are now rejected in both the file and diff translation paths rather than clamped: a negative offset (e.g. an indexOf miss) is invalid addressing, not a clampable range, so dropping it matches the documented behavior for out-of-range spans. Added regression cases to the dropped-spans tests on both paths.
Add onDecorationClick, onDecorationEnter and onDecorationLeave options so consumers can attach behavior to span decorations the same way they do with onTokenClick/onTokenEnter/onTokenLeave. Callbacks receive the consumer's original decoration object, the rendered span element, the line number and (for diffs) the side, plus the raw pointer event. To resolve events back to decoration objects, the rendered span's data-span-decoration attribute now carries the decoration's index in the consumer's spanDecorations array, and the InteractionManager looks it up through a resolver wired by File/FileDiff, which own that array. Events flow through the existing InteractionManager pointer pipeline: the target resolver picks up the decoration span from the composed event path alongside line and token info, hover transitions are tracked like token hover, and the new callbacks participate in the listener-attachment checks. The callbacks are also registered as CodeView shared callback keys so CodeView consumers get the item context argument appended, consistent with the token callbacks.
|
Pushed a second commit adding interaction callbacks for decorations: Why: decorations make natural anchors for review-assist UI (click a flagged span → explainer card, hover a diagnostic underline → message). That's possible today with Shape (mirrors the token callbacks): new FileDiff({
onDecorationClick({ decoration, decorationElement, lineNumber, side }, event) {
// `decoration` is the consumer's original DiffSpanDecoration object
},
onDecorationEnter(props, event) { /* hover in */ },
onDecorationLeave(props, event) { /* hover out */ },
});Mechanics: the rendered span's Covered by |
Add a Span Decorations docs section modeled on Token Hooks: a content.mdx covering addressing semantics, styling through unsafeCSS, overlap behavior with the built-in intra-line diff highlight, and the interaction callbacks, plus React and Vanilla JS code examples behind the usual tab switcher. Wire the section into DocsPage after Token Hooks, register the tabs component for MDX, add cross-reference sentences in the React API and Vanilla JS API sections, add spanDecorations entries to the shared render-prop example files, and add a feature bullet to the package README.
Math.min only capped the upper bound, so a negative spanStart (for example from an indexOf miss) was forwarded to Shiki, which treats negative character positions as offsets from the end of the line and would wrap the wrong text. Reject these spans in both the file and diff translation paths, matching the documented behavior of dropping invalid ranges.
|
Hey sorry, this is a huge PR with massive implications and generally speaking we have checkbox about reaching out to us to discuss feature request so we can understand what is needed, who needs it and why. Also it would be great to know how much of this was AI generated (our standard PR form has all of this, but it's not included in your top level description). Any chance we could maybe move this to an issue to discuss further? |
|
Ultimately I would like to know more about who you are and what your needs/requirements are before deciding on how to move forward with feature requests. |
|
@amadeus Apologies, I totally missed the contributing doc! Only intended for this to be a request/proposal along with a strawman implementation, will close it for now and follow up with a proper proposal per the guidelines. Appreciate the heads up |
What this does
Adds a public, declarative way to style — and now interact with — arbitrary character ranges within a line, for both code and diff views. It reuses the shiki decorations pipeline that already powers the internal intra-line word/char diff highlighting, so this is exposing existing machinery rather than new rendering.
The range wraps in a span carrying the consumer
classNameplus adata-span-decorationattribute. Structured input only — no raw HTML or CSS strings — consistent with the package's existingunsafeCSS/prerenderedHTMLposture.Why
We consume
@pierre/diffsin production for code review and need review-assist overlays: highlighting high-risk spans, muting low-relevance code, anchoring AI-assist affordances to sub-line selections, and word-granularity classifier signals. Per-line annotations plus gutter slots cover line granularity, but only the engine can own sub-line styling because it owns tokenization. Happy to beta this API and iterate on the shape.API shape and naming
SpanDecoration/DiffSpanDecorationfollow thecreateDiffSpanDecorationconventions (spanStart/spanLengthcharacter offsets);lineNumberis 1-based likeLineAnnotation, and the diff variant uses the sameside: 'deletions' | 'additions'addressing asDiffLineAnnotation.lineAnnotations— render/hydrate props onFile/FileDiff/UnresolvedFile, aspanDecorationsfield onCodeViewitems, props on the React wrappers, and options on the SSR preload helpers — rather than as aBaseCodeOptionsentry, which is shared across all files in aCodeView.spanDecorations(notdecorations) to stay clear of Decorations v3 #459, which usesdecorationsfor line-level bars/backgrounds. The two are complementary: that PR covers line granularity, this one covers character granularity.onDecorationClick/onDecorationEnter/onDecorationLeavemirroronTokenClick/onTokenEnter/onTokenLeave: same option placement (InteractionManagerBaseOptions), same props-plus-raw-event signature, same CodeView item-context overloading. Callbacks receive the consumer's original decoration object plus the rendered span element to anchor popovers/tooltips against.Precedence when overlapping internal diff highlights
Consumer spans are pushed after the engine's own
data-diff-spanitems, so when ranges overlap shiki nests the consumer span inside the diff-highlight span and both classes apply — internal highlighting wins by default but consumers can intentionally layer on top via CSS specificity. Covered by a test.Implementation notes
renderDiffWithHighlighterindexes decorations per side by line number and translates them to bucket-local shikiDecorationItems duringiterateOverDiff, so they land correctly in both the single-bucket and per-hunk (partial/plain-text) paths.renderFileWithHighlighterdoes the same translation including the windowed-highlight offset.spanDecorationsparticipates inRenderFileOptions/RenderDiffOptionsand the equality helpers (newareSpanDecorationsEqual, structural comparison). Cached or worker-pool ASTs highlighted without the current decorations are invalidated and re-highlighted locally; when no decorations are set, behavior and cache hits are unchanged.data-span-decorationattribute carries the decoration's index in the consumer array; theInteractionManagerresolves it via a resolver wired byFile/FileDiffand rides the existing pointer pipeline (target resolution from the composed event path, token-style hover transition tracking, listener-attachment checks).createSpanDecorationis exported alongsidecreateDiffSpanDecorationfor consumers buildingDecorationItems manually.How it's verified
test/spanDecorations.test.ts(bun test): file render with the class on the exact range; coexistence with word-level diff highlighting on the same change line on both sides; out-of-range/zero-length dropping; equality-helper semantics.test/spanDecorations.interactions.test.ts:onDecorationClickresolves the original decoration object on file views;onDecorationEnter/onDecorationLeavefire on hover transitions with correct side on diff views.tsgo --noEmit,oxfmt, andoxlintare clean forpackages/diffs.Docs
Included, modeled on the Token Hooks pattern: a Span Decorations section on the docs site (content + React/Vanilla tabbed examples, wired into the docs page after Token Hooks), cross-reference sentences in the React API and Vanilla JS API sections,
spanDecorationsentries in the shared render-prop example files, and a feature bullet in the package README. Verified by rendering /docs locally — the section, anchor, and highlighted examples all render.After merge
If the shape looks right, an interactive home-page example (like the Annotations one) would be a natural follow-up — happy to send it.