GitLineDiffA Visual Studio Code extension that makes single-line files diffable. Many repositories store data files — especially JSON — as a single, minified line. Git diffs of those files are nearly useless: any change marks the entire line as modified, so you can't see what actually changed. GitLineDiff adds a custom Source Control view that lets you open a diff in which both sides are pretty-printed in memory before being compared. The files on disk are never modified — all transformations happen in virtual documents.
Features
The view lists all changed files in the working tree. Files with a matching
formatter are annotated (e.g. Ways to open a pretty diff
The commit graphThe graph opens as a full-width panel in the editor area (like Git Graph), rendering your commit history with branch/merge lanes. It's our own webview — not VS Code's built-in graph — because the built-in graph's context menu is gated behind a proposed API that installed extensions can't use. Owning the panel lets us put the pretty diff one click away from any commit. Diffs are commit-vs-first-parent (the root commit is compared against the empty tree).
InstallationFrom source (recommended during development)
Then press F5 in VS Code to launch an Extension Development Host with GitLineDiff loaded. Packaging a
|
| Command | Description |
|---|---|
npm install |
Install dependencies. |
npm run compile |
Type-check and build to out/. |
npm run watch |
Rebuild on change. |
npm run lint |
Run ESLint over src/. |
| F5 | Launch the Extension Development Host. |
Requirements: Node.js 18+, VS Code 1.85+. The built-in Git extension
(vscode.git) must be enabled — it is declared as an extension dependency.
Configuration
All settings live under the gitlinediff.* namespace and update the view and
any open diffs live.
| Setting | Default | Description |
|---|---|---|
gitlinediff.json.fileExtensions |
["json"] |
Extensions treated as standalone JSON and pretty-printed. |
gitlinediff.embeddedJson.enabled |
true |
Expand single-line JSON strings embedded inside other files. |
gitlinediff.embeddedJson.fileExtensions |
["yaml", "yml"] |
Extensions scanned for embedded JSON values. |
gitlinediff.embeddedJson.autoDetect |
true |
When no keys/keyPattern are set, expand any value that parses as a JSON object/array. |
gitlinediff.embeddedJson.keys |
[] |
Restrict expansion to these exact key names. |
gitlinediff.embeddedJson.keyPattern |
"" |
Optional regex matched against key names. |
Eligibility rule for embedded JSON: a value is expanded only if it parses as
a JSON object/array and its key is eligible. A key is eligible if keys
contains it or keyPattern matches it; if neither is configured, autoDetect
makes every key eligible.
Example
Given this YAML in the working tree (a real, single-line value):
metadata:
name: demo
attributeValue: '{"a":1,"b":{"c":2},"d":[1,2]}'
GitLineDiff renders the diff side as:
metadata:
name: demo
attributeValue: |-
{
"a": 1,
"b": {
"c": 2
},
"d": [
1,
2
]
}
Only the matched value line is rewritten (as a YAML block scalar); every other line is preserved exactly, so the diff stays focused on what actually changed. To expand only specific keys, set:
"gitlinediff.embeddedJson.keys": ["attributeValue"]
How it works
Git integration (
src/gitApi.ts) Acquires the built-in Git extension API (vscode.git, version1), lists working-tree changes, and reads file content from both the working tree andHEAD(viaRepository.show('HEAD', path)).Source Control view (
src/treeView.ts) ATreeDataProviderpopulates thegitLineDiffViewview with the working tree's changed files, annotating each with the formatter that applies (if any). Selecting an item runs thegitlinediff.openDiffcommand.Virtual documents (
src/extension.ts) ATextDocumentContentProviderregistered for the customgitlinediffscheme produces pretty-printed content on demand and in memory. Each virtual URI encodes the original file path and which revision it represents (headorworking):gitlinediff:/abs/path/to/file.json?ref=head&src=/abs/path/to/file.jsonKeeping the original path as the URI path means VS Code infers the correct language for syntax highlighting.
Diff command (
gitlinediff.openDiff) Builds two virtual URIs (HEADon the left, working tree on the right) and opens them with:vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `GitLineDiff: ${fileName}`);Both sides are transformed through the formatter registry first, so the diff compares readable, multi-line representations.
Architecture overview
src/
├── extension.ts Activation, virtual-document provider, commands, config wiring
├── config.ts Typed reader for gitlinediff.* settings
├── gitApi.ts Typed wrapper around the built-in Git extension (changes, refs, commits)
├── git.d.ts Vendored, minimal type declarations for vscode.git
├── treeView.ts Source Control TreeDataProvider + formatter annotation
├── graphLayout.ts Pure commit-graph lane/line layout algorithm
├── graphView.ts Commit-graph webview panel (renders the layout, opens commit diffs)
└── formatterRegistry.ts Pluggable formatters (JSON + embedded-JSON), built from config
Ref-aware virtual documents
A virtual URI encodes the file path plus a ref: working (read from disk) or
any git ref (HEAD, a commit hash, a parent). This single mechanism powers both
the working-tree diff (HEAD vs working) and commit diffs
(parent vs commit) — and a commit's multi-file diff is opened via
_workbench.openMultiDiffEditor, falling back to individual diff tabs if that
command is unavailable.
Design principles:
- Separation of concerns — Git access, UI, and content transformation are independent modules.
- Strict typing — strict TypeScript, no
any; the Git API is fully typed. - Read-only & non-destructive — transformations live only in virtual docs.
- Extensible — formats are added through a registry, not by editing core logic.
Adding a formatter
Formatters are pure functions that turn raw content into a pretty-printed
string, falling back to the original content on failure. Register them in
createDefaultRegistry() (in src/formatterRegistry.ts) or anywhere you have
the registry instance.
XML
registry.register({
id: 'xml',
extensions: ['xml', 'svg'],
format: (content) => prettifyXml(content), // your formatter of choice
});
YAML
import * as yaml from 'yaml';
registry.register({
id: 'yaml',
extensions: ['yaml', 'yml'],
format: (content) => {
try {
return yaml.stringify(yaml.parse(content));
} catch {
return content; // never throw — fall back to raw content
}
},
});
TOML
import * as toml from '@iarna/toml';
registry.register({
id: 'toml',
extensions: ['toml'],
format: (content) => {
try {
return toml.stringify(toml.parse(content));
} catch {
return content;
}
},
});
To also show the new file type in the view, widen the filter in
src/treeView.ts (replace jsonOnlyFilter), e.g.:
const allowed = new Set(['json', 'xml', 'yaml', 'yml', 'toml']);
export const supportedFilter: FileFilter = (file) =>
allowed.has(file.fileName.split('.').pop()?.toLowerCase() ?? '');
License
MIT
