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.vuevia a newblameGroupsprop;source_viewer.vueslicesblameDataper chunk usingblameGroupsForChunkbefore 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-basedblameInfocomputed — no pixel tracking needed - The
highlightedevent is removed fromchunk.vue
Skeleton loader
blame_skeleton_loader.vuenow renders inside the chunk's blame column (col 1 of the subgrid) instead of being absolutely positioned with a measuredtranslateYoffsetloadingChunksremains but the offset calculation it previously drove is gone;hasBlameDataForChunkis 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.vueinto a standalone component - Consumed by both
source_viewer.vue(new grid path) andblame_info.vue(untouched, still used bysimple_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, andtoggleBlameLineBordersare still used bysimple_viewer.vue
simple_viewer.vue
- Untouched. Its blame path shares
blame_info.vueand 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
- Enable the feature flag:
Feature.enable(:inline_blame) - Open a large file with blame on — the Linux
MAINTAINERSfile is the easiest reproduction:https://staging.gitlab.com/jerasmus/linux/-/blob/master/MAINTAINERS?blame=1 - As soon as the page loads, press End to jump to the bottom
- Confirm blame groups align with their lines as chunks finish loading — none should appear at the wrong position
- Scroll back up and confirm alignment is maintained
- Toggle blame off and on — confirm no layout shift
- Resize the blame column and reload — confirm width is persisted
- 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.vueis unchanged and snippets blame path is unaffected- Existing tests updated; no orphaned tests remain