Git-blame-style inline diff gutter for CodeMirror 6 with a VSCode-like peek view (dirty diff / Source Control decorations).
A plug-and-play CodeMirror 6 extension: install it, import gitGutter,
pass the git baseline content, and mount it in your extensions array. It
computes a line-based diff between the current editor content and the baseline,
renders colored gutter markers (added / modified / deleted), and opens an
in-editor peek view when a marker is clicked — complete with
Stage / Revert / Next / Previous controls.
- Plug-and-play: a single
gitGutter(config)call returns a ready-to-mountExtension. All styling (gutter, markers, peek view) is injected automatically via CodeMirror style modules — no CSS file to import, no manual DOM wiring. Peer dependencies (@codemirror/state,@codemirror/view) are installed automatically by npm ≥ 7. - Pure function of
(baseline, currentDoc)— the plugin never runs git. The host app provides the baseline through thebaselineconfig option (e.g. the result ofgit show HEAD:path/to/file). - VSCode-style gutter markers: green = added, cyan = modified, small red triangle = deleted (attached at the line boundary, exactly like VSCode).
- Peek view that "splits" the editor (native CM6 block widget, not a modal)
and renders a unified diff of the change with surrounding context —
old/new line numbers and tinted
+/-rows, VSCode dirty-diff style. - Stage delegates a precise hunk to a host callback (
git apply --cachedis the host's job); Revert is a purely local CM6 document transaction (no git involved). - Debounced diff recomputation (default 150ms) via a
ViewPlugin, so typing stays smooth even on large files. - Theme-agnostic: the peek view is fully transparent — it inherits the
editor background and text color (
currentColor) automatically, and auto-constrains its width so it never tucks under a minimap, just like VSCode. Marker / diff-tint colors are exposed as CSS variables for optional overrides. - Zero runtime dependency besides
diff; ESM + CJS dual build, tree-shakeable.
NOTICE: The gutter + peek-view UX in this package replicates the Source Control Decorations (dirty diff) feature of VSCode (conceptual architecture of
dirtydiffDecorator.ts). It was written from scratch on top of CodeMirror 6 primitives and does not copy VSCode code.
npm install @fazelstudio/codemirror-gitgutterPeer dependencies (installed automatically by npm ≥ 7, or install manually):
npm install @codemirror/state @codemirror/viewimport{EditorView,basicSetup}from'codemirror';import{gitGutter}from'@fazelstudio/codemirror-gitgutter';constview=newEditorView({doc: currentFileContent,extensions: [basicSetup,gitGutter({baseline: headFileContent,// host app: `git show HEAD:path/to/file`onStageHunk: (hunk)=>myBackend.stageHunk(filePath,hunk),}),],parent: document.querySelector('#editor'),});That's it. Click a gutter marker to open the peek view; click it again to
close it. If your host app cannot provide an onStageHunk callback, the Stage
button is disabled automatically.
The main entry point. Returns a single Extension ready to mount.
| Option | Type | Default | Description |
|---|---|---|---|
baseline | string | — | File content at the git baseline (required). |
onStageHunk | (hunk: Hunk) => void | — | Called when the Stage button is pressed. When omitted, the Stage button is disabled. |
colors | { added?, modified?, deleted? } | VSCode palette | Marker colors (CSS values). |
debounceMs | number | 150 | Diff recompute delay after a document change. 0 = synchronous. |
gutterWidth | string | '4px' | Gutter column width. |
typeChangeType='added'|'modified'|'deleted';interfaceHunk{type: ChangeType;fromA: number;// first baseline line (1-based, inclusive)toA: number;// last baseline line (toA < fromA for pure additions)fromB: number;// first current-document line (1-based)toB: number;// last current-document line (toB < fromB for pure deletions)baselineText: string;// baseline text for this hunk (used by revert & peek view)}All commands follow the (view: EditorView) => boolean convention:
gitGutterGoToNextChange/gitGutterGoToPreviousChangegitGutterToggleWidgetgitGutterRevertChangegitGutterStageChange
Optional keybindings (not registered automatically — mount explicitly):
import{keymap}from'@codemirror/view';import{gitGutterKeymap}from'@fazelstudio/codemirror-gitgutter';
extensions: [keymap.of(gitGutterKeymap)]Defaults: Alt-F3 next change, Shift-Alt-F3 previous change,
Mod-Shift-Backspace revert.
For host-side metadata UIs (e.g. a minimap that colors changed lines), the plugin exports its state:
import{hunksField,hunkAtLine,baselineContentFacet}from'@fazelstudio/codemirror-gitgutter';// Current hunks (updated via the plugin's debounced diff):consthunks=view.state.field(hunksField,false)??[];// The hunks covering a given 1-based line number:consthunk=hunkAtLine(view.state,lineNumber);gitGutterLineDecorations()— subtle background highlight on added/modified lines (not included by default).gitGutterDarkTheme/gitGutterLightTheme— deprecated no-ops. The peek view follows the editor theme automatically; keep them only to avoid breaking old imports, then remove them.
Marker and diff-tint colors are semantic and exposed as CSS variables.
Override them on .cm-editor (or the editor wrapper):
.cm-editor {
--cm-gitgutter-added-color:#3fb950;
--cm-gitgutter-modified-color:#58a6ff;
--cm-gitgutter-deleted-color:#f85149;
--cm-gitgutter-diff-added-bg:rgba(63,185,80,0.22);
--cm-gitgutter-diff-removed-bg:rgba(248,81,73,0.22);
}The peek view background is fully transparent — it automatically inherits the
editor theme (background + currentColor), so no theme presets are needed.
Its width is capped automatically to the visible editor area (stopping before
a minimap when present).
The plugin provides the data; the host app runs git. Example with
simple-git:
import{git}from'simple-git';asyncfunctionsetupGutter(view: EditorView,filePath: string){constrepo=git();constbaseline=awaitrepo.show([`HEAD:${filePath}`]);view.dispatch({effects: EditorState.reconfigure.of([
...existingExtensions,gitGutter({
baseline,onStageHunk: async(hunk)=>{// Apply the hunk to the index. Real implementations typically use// `git apply --cached` with a patch built from `hunk`.awaitrepo.apply(['--cached',buildPatch(filePath,hunk)]);// After staging, reconfigure with the new baseline.constnewBaseline=awaitrepo.show([`HEAD:${filePath}`]);view.dispatch({effects: EditorState.reconfigure.of([
...existingExtensions,gitGutter({baseline: newBaseline, onStageHunk }),]),});},}),]),});}The baseline can also be updated reactively at any time via
EditorState.reconfigure.
Run the local playground (no git backend — two hardcoded strings):
npm install
npm run dev # open http://localhost:5173The playground has buttons to simulate modified / added / deleted / mixed
content plus a log for Stage actions. To deploy to GitHub Pages, run
npx vite build (output lands in examples/basic/dist).
| Script | Description |
|---|---|
npm run build | Build ESM + CJS + types via tsup into dist/ |
npm test | Run unit & integration tests (vitest) |
npm run lint / npm run typecheck | Type-check with strict TypeScript |
npm run dev | Run the Vite playground |
npm run publish:dry | Dry-run publish to npm |
MIT — see LICENSE.
