Skip to content

How the Staging View Works

github-actions[bot] edited this page Oct 18, 2026 · 1 revision

The staging view shows a file of the Changes diff in three panes: HEAD, the index and the work tree, with bands joining each side's changes to the index in the middle. It reuses the 3-pane diff of merge commits. For the user side, see Staging View.

Why we need it

The two-pane diff shows either the staged or the unstaged half of a file, so a file with both needs two clicks to understand. The staging view shows both halves at once around the index, and stages or unstages from the bands. We already had every piece: the 3-pane frame and bands of the merge tool and the merge commit view.

How it works

ChangesDiff.svelte (and the status bar's Changes tab, ChangesTab.svelte) shows ThreePaneDiff.svelte when Diff layout is side by side, settings.changesDiffPanes is "three" and stagingViewFits says the file is a plain modified file (not new, deleted, renamed, untracked or conflicted). StagingDiffs (views/changes/stagingDiffs.svelte.ts) loads both diffs:

flowchart LR
    S[get_file_diff staged] --> L[HEAD against the index: left]
    U[get_file_diff unstaged] --> W[workTreeSide: flip it]
    W --> R[work tree against the index: right]
    L --> T[ThreePaneDiff]
    R --> T
Loading

ThreeWayEditors takes each side pane against the middle. The unstaged diff runs the other way (index to work tree), so workTreeSide swaps its texts, EOLs and hunks (flipHunks). The options { rightNewer: true, conflicts: false } keep its colors right (lines the work tree adds are green) and never call a change a conflict.

A band's arrow calls ThreeWayEditors.taken(side, index), which takeChange builds: the index text with the change's lines from that side. changesSelection.applyDiffChange writes it to the index, the same path as the two-pane arrows. The new status reloads both diffs; StagingDiffs keeps a diff whose texts did not change, and ThreePaneDiff keeps the middle pane's scroll position when it rebuilds.

For the colors, paneTones (threeWay.ts) makes the left pane older and the right newer when rightNewer is set; parentMarks and resultMarks give each mark that tone, the middle the opposite end of its side, and toneColors (mergeExtensions.ts) draws old in --diff-removed and new in --diff-added. MergeFrame's colors fades each band from its older pane to its newer one. A merge's conflicts keep their color, and the merge tool sets no tone.

Every diff draws the same DiffToolbar.svelte: the two-pane DiffView and ThreePaneDiff pass the counter, the layout (three, sideBySide or inline), the Three panes tooltip and what they offer, so a button or a fix reaches both. threeWayBlame.ts puts Blame on one pane (blame.pane: the work tree here, the merge result for a merge commit). Open File shows when the caller passes workingFile; it calls openWorkingFile (diff/openWorkingFile.ts, shared with DiffView) at ThreeWayEditors.lineOnScreen("right"), the working tree's line.

Resizing the panes

MergeFrame saves two boundaries, settings.threePaneSplit, as the panes' flex-grow shares. Each connector's outer edge has a SplitHandle (the two-pane diff's handle) in a box that leaves out the other connector, so its ratio is a share of the panes' width. moveThreeSplit (library logic/threeSplit.ts) keeps every pane at 15% or more. The merge tool shares the frame and the setting.

Where the code lives

File What it does
src/lib/views/changes/stagingDiffs.svelte.ts Which files fit, loading both diffs, the flipped work tree side
src/lib/views/changes/ChangesDiff.svelte, views/git/ChangesTab.svelte Pick the staging view or the two-pane diff
src/lib/diff/ThreePaneDiff.svelte, threeWayBlame.ts The frame for merges and staging, and its pane blame
src/lib/diff/DiffToolbar.svelte, diffToolbar.ts The one toolbar of every diff, and its counter
src/lib/diff/threeWay.ts ThreeWayOptions, flipHunks, takeChange

Design decisions

  • Off by default. The two-pane diff stays the Changes default; the choice is remembered once made.
  • A setting, not browser storage. The pane counts are preferences in settings.json (changesDiffPanes, mergeDiffPanes), so Settings > Git can show them and they move with the settings file. Three columns show only while Diff layout is side by side; the toolbar's Side by side button means 2 columns (diff/diffPanes.ts). The old localStorage flags are not read back, so a changed choice starts at the default once.
  • Plain modified files only. A new or deleted file has an empty side, and a rename has two paths; the two-pane diff shows them better.

Tests

  • src/lib/views/changes/stagingDiffs.test.ts: which files fit, the flipped work tree side and the index's line ending (indexEol).
  • threeSplit.test.ts (UI library) and settingsData.test.ts: the panes' boundaries, their minimum and the saved split.
  • src/lib/diff/threeWay.test.ts: the staging colors, no conflicts, and staging and unstaging with takeChange.

Bugs we fixed

The three panes had no Open File button.

  • The issue: the two-pane diff has Open File, but switching to three panes (staging or a merge commit) lost it.
  • Why it happened: ThreePaneDiff builds its own toolbar, and Open File lived only in DiffView.
  • The fix and why we chose it: the open code moved to openWorkingFile.ts, which both views call, so the check for a missing file and the toast stay the same in both.

The Changes tab had no Three panes button.

  • The issue: the diff in the Changes tab (status bar > N changes) showed only Side by side and Inline, so a modified file could not open in the staging view there.
  • Why it happened: only ChangesDiff.svelte passed onThreePanes and drew ThreePaneDiff; the tab compares with HEAD in its own view.
  • The fix and why we chose it: the tab uses the same stagingViewFits, StagingDiffs and changesDiffPanes setting, so both places look and switch alike. Its Stage and Unstage write the index pane's text with stageContent, in the index's own line ending (indexEol), one click after another.

The three-pane toolbar drifted from the two-pane one.

  • The issue: three panes had no Blame, no divider after the counter and an extra change count, and the Collapse tooltip differed.
  • Why it happened: ThreePaneDiff drew its own toolbar, a copy of DiffToolbar that fixes did not reach.
  • The fix and why we chose it: DiffToolbar takes plain props instead of the two-pane editors, and both views draw it, so the buttons, order and spacing live in one place.

The three panes could not be resized.

  • The issue: the two-pane diff lets you drag the line between its sides, but the three panes kept equal widths, so long lines were cut off in every pane.
  • Why it happened: MergeFrame gave each pane flex: 1 and had no handle; only DiffFrame had a SplitHandle.
  • The fix and why we chose it: MergeFrame holds two boundaries (threePaneSplit) with a SplitHandle on each connector's outer edge, reusing the two-pane handle and its pointer math, so dragging, keys and double-click behave the same. Being in the library frame, it reaches the merge tool and merge commits too.

Keeping this page in sync

  • Retake diff-staging-view.png when the view looks different.

Home

Using Nibtree

Developers

How features work

Clone this wiki locally