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 BlameInfo and BlameSkeletonLoader with BlameColumnResizer (from !248529 (merged))
  • Declares the three-column grid (blame | line numbers | code) once via blameGridStyling; each chunk inherits the tracks through subgrid
  • Adds blameGroupsByChunk computed — slices normalised blame data per chunk using createBlameSliceBuilder, which memoises per-chunk arrays so only chunks whose blame actually changed re-render
  • Removes chunkOffsets, updateChunkOffsets, isBlameLoading, and the blameData watcher — no pixel tracking or layout-shift workarounds needed
  • Removes the @highlighted event handler and the blameData = [...blameData] force-refresh
  • Passes blameGroups, isBlameLoading, and projectPath down to each <chunk>
  • Removes the duplicate-guard on blameData.push — normalizeBlameGroups absorbs 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 highlighted emit — no longer needed
  • Removes codeStyling (the marginLeft derived from lineNumbers.offsetWidth) — the gutter is a real grid column now
  • Adds chunkGridStyling (explicit gridTemplateRows) so row-span-full resolves correctly against the explicit grid
  • Adds blameCellStyling, blameCellLabel, blameSeparatorRows, rowStyling, and lineNumberStyling helpers

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-based rowStart/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, and toggleBlameLineBorders — still used by simple_viewer.vue

simple_viewer.vue

  • Untouched. Its blame path shares blame_info.vue and the existing utils.

files.scss

  • Adds .source-line-grid — sets grid-auto-rows to --source-line-height so 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-full divs placed by gridRow), replacing toggleBlameLineBorders' 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 isBlameLoading and blameGroups.length
  • simple_viewer.vue is unchanged and the snippets blame path is unaffected
  • Existing tests updated; new tests cover normalizeBlameGroups, blameGroupsForChunk, createBlameSliceBuilder, and the chunk's blame rendering
Edited by Joseph Fletcher

Merge request reports

Loading
Loading