Render inline blame in a CSS subgrid instead of an overlay
What does this MR do and why?
Replaces absolute-positioning for the inline blame gutter with CSS
Grid/subgrid placement, built on the normalizeBlameGroups and
blameGroupsForChunk helpers added in MR 1 of this split,
!251528 (merged).
Previously, blame entries were positioned by measuring the DOM:
calculateBlameOffset called getBoundingClientRect on #LC{n} elements
and returned null for lines not yet rendered. Every arriving blame
response re-derived pixel offsets for the whole column, and blame loading
for off-screen lines caused visible layout shift. A workaround
(@highlighted="blameData = [...blameData]") patched the worst case but
left the underlying architecture in place.
With grid placement, blame position is pure data (row start/span), so nothing needs to be measured and nothing shifts as data arrives.
Notable changes:
source_viewer.vue: declares a three-column grid (blame | line numbers | code) once on the container viablameGridStyling, publishing the blame column width as a--blame-column-widthcustom property. Each chunk inherits the tracks throughsubgrid. SwapsBlameInfo+BlameSkeletonLoaderforBlameColumnResizer. Adds ablameGroupsByChunkcomputed that normalizes and slices blame data per chunk. DeleteschunkOffsets,updateChunkOffsets,isBlameLoadingstate, theblameDatawatcher, the@highlightedhandler and its force-refresh, and the duplicate-guard onblameData.push(now handled bynormalizeBlameGroups).chunk.vue: new propsblameGroups,isBlameLoading,projectPath. Renders blame cells, the skeleton loader, the sticky gutter background, and group-separator borders inside the chunk's subgrid column 1. Group separators are now declarative grid elements, replacingtoggleBlameLineBorders's imperative DOM class toggling. RemovescodeStyling(amarginLeftderived from measuringlineNumbers.offsetWidth) and thehighlightedemit. Adds explicitgridTemplateRowssorow-span-fullresolves against the explicit grid. Adds screen-reader labels for blame cells.files.scss: adds.source-line-grid(row sizing tied to--source-line-height) and.blame-age-indicator(left border driven by--blame-age-color).
simple_viewer.vue is deliberately untouched — it still uses
blame_info.vue and the legacy utils, so none of that code can be removed
yet. That migration is a separate follow-up (see breakdown below).
This MR targets 589723-blame-group-utils, the branch of !251528 (merged).
Once that MR merges, this
MR's target should be changed to master.
Related to #589723 (closed)
This is the second of three MRs split out of !248531 (closed) following review feedback asking for smaller diffs.
Temporary performance tradeoff
This MR introduces a temporary re-render cost, removed by MR 3, !251530 (merged).
- Baseline: on master, blame is rendered by a single
blame_info.vuecomponent at thesource_viewerlevel rather than inside each chunk, so chunk components are never re-rendered when blame data arrives. This MR changes that. - What triggers it: the new
blameGroupsByChunkcomputed property insource_viewer.vuerebuilds ablameGroupsarray for every chunk, not just the visible ones. Each time blame arrives for one chunk, every chunk gets a new array by identity, so Vue re-renders every chunk component, including ones whose blame didn't change. - Scale:
LINES_PER_CHUNKis 70, so a 7,000-line file has 100 chunk components re-rendering on every blame fetch, which happens repeatedly as the user scrolls. - Fix: MR 3 adds
createBlameSliceBuilder, a per-chunk cache that reuses the previous array reference when a chunk's slice is unchanged, so Vue skips re-rendering it. - Why split out: the cache holds mutable cross-call state, the piece most likely to hide a stale-cache bug, so it gets its own review instead of being folded in here.
- Not benchmarked: this cost hasn't been measured. It only affects re-renders; correctness and rendering output are identical either way.
Given that, please review this MR and MR 3 (!251530 (merged)) together and merge them back-to-back rather than leaving a gap between them. That keeps the smaller-diff benefit of splitting the work without leaving a meaningful regression window open. If you'd rather they land as a single MR instead, that works too — just say so.
Screenshots or screen recordings
How to set up and validate locally
The inline_blame feature flag is default_enabled: true, so no flag
toggle is needed.
- Open a blob page on any project and click the blame toggle in the file header.
- Scroll so that chunks further down the file load their blame data off-screen.
- Confirm there is no layout shift as blame loads, and that blame cells stay aligned with their corresponding lines.
- Confirm the blame column resizer still works.
Implementation breakdown
| Step | MR | Status |
|---|---|---|
| Blame group normalization and per-chunk mapping (utils only, no callers) | !251528 (merged) | Merged |
| Render inline blame in a CSS subgrid instead of an overlay | (this MR) | |
| Cache per-chunk blame slices across renders | !251530 (merged) | |
Migrate simple_viewer.vue off blame_info.vue, remove legacy blame positioning code |
TBD | Planned |