Repository navigation
How the Connected Diff Works
The side by side diff shows each side as plain code, with no blank padding, and draws a band between the sides for every change. This page explains how. The rest of the diff is in How diffs work; the user side is in Diffs.
@codemirror/merge's MergeView lines the two sides up by adding blank spacers to the shorter side, and both sides scroll in one container. When one side gains many lines, the other side shows a big empty gap, and it is hard to see where a block came from or went. A connected diff keeps both texts whole, scrolls each side on its own while keeping matching lines level, and draws a curved band from a change's lines on the left to its lines on the right. The user asked for that look.
We keep MergeView, because its chunks, line and character highlights, change gutter, getChunks and mergeViewSiblings power folding, staging and the overview ruler. Only three things change:
-
No padding.
unpadded(view)insplitLink.tsreplaces the view's privatemeasure, which only adds the spacers (it also placed the old revert buttons, which we no longer use). -
One scroller per side.
DiffFrame(UI library) getsribbons. With them it gives each.cm-editorthe full height and its own scrolling.cm-scroller, overriding the base theme that makes the editors grow inside one scroller. The gap between the sides is themergeView.gaptoken. -
Bands and synced scrolling.
SplitLinklistens to both scrollers.
flowchart LR
S[scroll on one side] --> C[line at its center]
C --> M[mapLine through diffAnchors]
M --> O[scroll the other side to that line]
S --> R[schedule ribbons]
R --> B[MergeConnector in DiffFrame]
splitChunks.ts holds the pure parts:
-
chunkLines(chunk, a, b): a chunk's lines on each side, 0-based and end-exclusive. An empty range is the gap between two lines, so a band from it is a point that fans out to the added lines. An empty change after the last line sits after it. -
chunkKind: added, deleted or modified, for the band's color. -
diffAnchors: pairs of matching lines at every change's edges, used with the merge tool'smapLineandinvertAnchors. The line at the center of the scrolled side is mapped and the other side is scrolled to put that line at its center, the way the merge tool'sScrollSyncdoes it. A lock skips the scroll events the follower causes. -
copyChange: the edit behind a band's arrow. It is the same asMergeView's own revert: the chunk's lines with their line break, unless they end the text.
SplitLink.ribbons() measures the changes with lines on screen with the merge tool's span, in pixels from the frame body's top, and drops bands far off screen. DiffEditors keeps them in ribbons, and DiffView passes them to DiffFrame, which draws the library's MergeConnector. The ruler's geometry updates also schedule the bands, so folds, edits and resizes move them.
The arrow on a band is Stage this change (Changes) or Unstage this change (Staged). DiffEditors.copyChange dispatches the edit with the revert user event, so the listener that writes the index runs as before. Read-only diffs draw the bands without arrows: MergeConnector shows a button only with its handler.
A fold names the code after it: the library's FoldRowWidget (editor/foldRow.ts, drawn for foldField.ts) reads scopeLabelAt(state, line) (foldScope.ts) for the line below the fold. That uses sticky scroll's scopeSource (the syntax tree, markdown headings or indentation) for the enclosing blocks, takes each block's first line (or the line before a lone {), and foldLabel.ts turns them into names: class Cart is Cart, public function store( is store(), and blocks such as if drop out. A PHP namespace or Java package line leads the label. With no named block the bar shows the count. The wavy line is a CSS mask, so it takes the theme's --border-strong. The 3-pane merge diff folds the same runs in all panes and mirrors each step (foldStep) to the others.
| File | What it does |
|---|---|
src/lib/diff/splitChunks.ts |
Lines, kinds, anchors and the copy edit of each change |
src/lib/diff/splitLink.ts |
unpadded, synced scrolling, band measuring, copy
|
src/lib/diff/diffEditors.svelte.ts |
Owns the link, ribbons and copyChange
|
src/lib/diff/foldLabel.ts, foldScope.ts
|
Fold labels |
ui/web/src/lib/components/DiffFrame.svelte |
ribbons, applySide, onapply, applyTitle; one scroller per side |
ui/web/src/lib/components/MergeConnector.svelte |
The bands and their optional buttons |
ui/web/src/lib/editor/splitGutters.ts (splitPaneGutters) |
Colored number rows, numbers facing the middle, the change buttons |
-
Keep MergeView. Writing our own two-editor diff would mean rebuilding highlights, the gutter and the chunk updates on edits. Turning off one private method is a small, contained dependency; a
@codemirror/mergeupdate that renamesmeasurebrings the padding back but breaks nothing else. - Center line sync. Mapping the center keeps the change you look at level on both sides, even inside a long added block.
-
Buttons beside the line numbers. A change's stage, unstage, apply and revert buttons sit in its side's gutter on its first line (the library's
splitPaneGuttersineditor/splitGutters.ts, built oneditor/mergeGutters.ts), next to the numbers but never over them. The old side's numbers move to its right edge (sideLineNumbers("after")) and each change's color runs through the numbers (splitGutterColors, fromgetChunks), so a band reads as one shape from text to text. With no buttons on it, the connector is narrow, and only the new side keeps a vertical scrollbar. The merge tool and the three-pane diffs draw the same way.
-
src/lib/diff/foldLabel.test.ts: names of classes, functions, methods and headings, skipped control blocks, namespaces. -
src/lib/diff/splitChunks.test.ts: changed, added and removed lines, changes at the end of the text, anchors, and staging and unstaging edits checked against the other side's text. -
ui/web/src/lib/editor/splitGutters.test.ts: each side's rows of a changed, added or removed block. - The look and the scrolling need a visual check:
diff-view,diff-hunk-staging,commit-tabandcolor-theme-high-contrastinscripts/screenshots.ts.
- A new way to change a side must keep the
revertuser event, or the index is not written. - After updating
@codemirror/merge, check thatmeasurestill only adds spacers.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode