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 via blameGridStyling, publishing the blame column width as a --blame-column-width custom property. Each chunk inherits the tracks through subgrid. Swaps BlameInfo + BlameSkeletonLoader for BlameColumnResizer. Adds a blameGroupsByChunk computed that normalizes and slices blame data per chunk. Deletes chunkOffsets, updateChunkOffsets, isBlameLoading state, the blameData watcher, the @highlighted handler and its force-refresh, and the duplicate-guard on blameData.push (now handled by normalizeBlameGroups).
  • chunk.vue: new props blameGroups, 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, replacing toggleBlameLineBorders's imperative DOM class toggling. Removes codeStyling (a marginLeft derived from measuring lineNumbers.offsetWidth) and the highlighted emit. Adds explicit gridTemplateRows so row-span-full resolves 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.vue component at the source_viewer level 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 blameGroupsByChunk computed property in source_viewer.vue rebuilds a blameGroups array 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_CHUNK is 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.

  1. Open a blob page on any project and click the blame toggle in the file header.
  2. Scroll so that chunks further down the file load their blame data off-screen.
  3. Confirm there is no layout shift as blame loads, and that blame cells stay aligned with their corresponding lines.
  4. 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) ⏳ In review
Cache per-chunk blame slices across renders !251530 (merged) ⏳ In review
Migrate simple_viewer.vue off blame_info.vue, remove legacy blame positioning code TBD Planned
Edited by Joseph Fletcher

Merge request reports

Loading
Loading