Refactor inline blame to use natural flow
What does this MR do and why?
Replaces the inline blame viewer's absolute positioning with a CSS Grid layout that puts blame, line numbers, and code in normal document flow.
Closes #589723 (closed)
Depends on !248529 (merged) (Extract BlameColumnResizer) — target branch will be updated to master once that MR merges.
The problem
Blame positions were computed by measuring the DOM — calculateBlameOffset called getBoundingClientRect on #LC{n} elements, which returned null for lines not yet rendered. Every arriving blame response re-derived the whole column, and loading blame for off-screen lines caused layout shift. The @highlighted="blameData = [...blameData]" workaround in !233596 (merged) patched the worst case but left the underlying architecture intact.
The fix
Blame rendering moves out of the standalone blame_info.vue column and into chunk.vue. Each chunk is a three-column subgrid — blame | line number | code — with the tracks declared once on the container in source_viewer.vue so columns stay aligned across chunk boundaries. Blame groups are placed with grid-row: span N derived from their rowStart/rowSpan, clamped to the lines their chunk actually renders. Grid placement is pure data — nothing shifts when blame loads for off-screen lines.
Notable changes
source_viewer.vue
- Replaces
BlameInfoandBlameSkeletonLoaderwithBlameColumnResizer(from !248529 (merged)) - Declares the three-column grid (
blame | line numbers | code) once viablameGridStyling; each chunk inherits the tracks throughsubgrid - Adds
blameGroupsByChunkcomputed — slices normalised blame data per chunk usingcreateBlameSliceBuilder, which memoises per-chunk arrays so only chunks whose blame actually changed re-render - Removes
chunkOffsets,updateChunkOffsets,isBlameLoading, and theblameDatawatcher — no pixel tracking or layout-shift workarounds needed - Removes the
@highlightedevent handler and theblameData = [...blameData]force-refresh - Passes
blameGroups,isBlameLoading, andprojectPathdown to each<chunk> - Removes the duplicate-guard on
blameData.push—normalizeBlameGroupsabsorbs duplicates
chunk.vue
- Accepts three new props:
blameGroups,isBlameLoading,projectPath - Renders blame cells, the skeleton loader, the gutter background, and group-separator borders directly inside the chunk's subgrid (column 1)
- Removes the
highlightedemit — no longer needed - Removes
codeStyling(themarginLeftderived fromlineNumbers.offsetWidth) — the gutter is a real grid column now - Adds
chunkGridStyling(explicitgridTemplateRows) sorow-span-fullresolves correctly against the explicit grid - Adds
blameCellStyling,blameCellLabel,blameSeparatorRows,rowStyling, andlineNumberStylinghelpers
utils.js
- Adds
normalizeBlameGroups— sorts and merges same-commit groups split across chunk fetch requests, preventing grid gaps from out-of-order or split arrivals; absorbs exact duplicates - Adds
blameGroupsForChunk— slices normalised data for a given chunk's line range, clamping groups that straddle chunk boundaries and computing 1-basedrowStart/rowSpan/hasSeparator - Adds
createBlameSliceBuilder— factory that returns a builder function holding a per-chunk cache; hands back the previous array identity when a chunk's slice is unchanged, confining re-renders to chunks whose blame actually moved - Retains
calculateBlameOffset,shouldRender, andtoggleBlameLineBorders— still used bysimple_viewer.vue
simple_viewer.vue
- Untouched. Its blame path shares
blame_info.vueand the existing utils.
files.scss
- Adds
.source-line-grid— setsgrid-auto-rowsto--source-line-heightso blame cells and line numbers align with code lines - Adds
.blame-age-indicator— declarative left border driven by--blame-age-color
Group separator borders
- Now declarative grid elements (
gl-col-span-fulldivs placed bygridRow), replacingtoggleBlameLineBorders' imperative DOM class toggling
Checklist
- Blame groups align with their lines via
rowStart/rowSpan— no pixel offset calculation - No layout shift when blame data loads for lines outside the viewport
- Skeleton loaders appear and disappear per chunk as data arrives, driven by
isBlameLoadingandblameGroups.length simple_viewer.vueis unchanged and the snippets blame path is unaffected- Existing tests updated; new tests cover
normalizeBlameGroups,blameGroupsForChunk,createBlameSliceBuilder, and the chunk's blame rendering