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)

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 lineno/span, 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 / chunk.vue

  • Blame rendering moves into chunk.vue via a new blameGroups prop; source_viewer.vue slices blameData per chunk using blameGroupsForChunk before passing it down
  • Removed @highlighted="blameData = [...blameData]" workaround — no longer needed since blame renders after chunk content is in the DOM
  • Removed chunkOffsets, updateChunkOffsets, and the offset-based blameInfo computed — no pixel tracking needed
  • The highlighted event is removed from chunk.vue

Skeleton loader

  • blame_skeleton_loader.vue now renders inside the chunk's blame column (col 1 of the subgrid) instead of being absolutely positioned with a measured translateY offset
  • loadingChunks remains but the offset calculation it previously drove is gone; hasBlameDataForChunk is retained as the signal for when to swap skeleton for real blame cells

blame_column_resizer.vue (new)

  • Extracts the drag handle, width persistence, and breakpoint behaviour from blame_info.vue into a standalone component
  • Consumed by both source_viewer.vue (new grid path) and blame_info.vue (untouched, still used by simple_viewer.vue)

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
  • Adds blameGroupsForChunk — slices the normalised data for a given chunk's line range
  • Nothing removed — calculateBlameOffset, shouldRender, and toggleBlameLineBorders are still used by simple_viewer.vue

simple_viewer.vue

  • Untouched. Its blame path shares blame_info.vue and the existing utils; a follow-up issue tracks removing the unreachable blame code from that file

Group separator borders

  • Now declarative grid elements, replacing toggleBlameLineBorders' imperative DOM class toggling

Fixes found during implementation

Resizer z-index / pointer-events — the resizer's positioning anchor spanned the full blame column at z-3 without pointer-events: none, making the gutter and the floating Duo "explain code" button unclickable. Fixed by scoping the interactive area to the drag handle only.

AiGenie selection leak — blame cells are now inside .file-content, which is the AiGenie text selection container. Without a guard, commit metadata (author names, commit messages) would be swept into the text sent to Duo. Fixed with gl-select-none on blame cells.


Screen recording

How to set up and validate locally

  1. Enable the feature flag: Feature.enable(:inline_blame)
  2. Open a large file with blame on — the Linux MAINTAINERS file is the easiest reproduction: https://staging.gitlab.com/jerasmus/linux/-/blob/master/MAINTAINERS?blame=1
  3. As soon as the page loads, press End to jump to the bottom
  4. Confirm blame groups align with their lines as chunks finish loading — none should appear at the wrong position
  5. Scroll back up and confirm alignment is maintained
  6. Toggle blame off and on — confirm no layout shift
  7. Resize the blame column and reload — confirm width is persisted
  8. Confirm the Duo "explain code" button in the gutter is clickable while blame is active

Checklist

  • Blame groups align with their lines via grid-row: span N — no pixel offset calculation
  • No layout shift when blame data loads for lines outside the viewport
  • Visual appearance matches the current design
  • Skeleton loaders appear and disappear per chunk as data arrives
  • Resizer drag handle, width persistence, and breakpoint behaviour work correctly
  • Duo "explain code" button is clickable while blame is active
  • Commit metadata is not included in AiGenie text selections
  • simple_viewer.vue is unchanged and snippets blame path is unaffected
  • Existing tests updated; no orphaned tests remain
Edited by Joseph Fletcher

Merge request reports

Loading
Loading