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). The panel is laid out as a table with resizable and reorderable columns:
Drag a column's right edge to resize it, or drag a column header onto another to reorder columns. Your layout is remembered across sessions. A toolbar above the table gives you a full git view:
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.yaml.fileExtensions |
["yaml", "yml"] |
Extensions treated as standalone YAML and pretty-printed. |
gitlinediff.structuredData.canonicalizeToJson |
true |
Render both JSON and YAML as canonical pretty JSON for cross-format comparison. Origin badges still show the on-disk format. |
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"]
Escaped JSON string helper
Some tracked config files store JSON as a single escaped line (double quotes
escaped, line breaks as \r\n). The helper script
scripts/json_to_escaped_string.py converts a normal .json file into that
format:
python scripts/json_to_escaped_string.py input.json output.txt
The script validates the JSON, pretty-prints it with 2-space indentation, applies the escapes, wraps the result in outer double quotes, and writes a single line to the output file. Example output:
"{\r\n \"openapi\": \"3.0.0\",\r\n \"info\": {\r\n \"title\": \"Example\"\r\n }\r\n}"
Requires Python 3.9+ (stdlib only). Clear errors are printed for missing files or invalid JSON.
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, YAML, embedded-JSON), built from config
Origin badges
Structured JSON and YAML files get a one-line banner at the top of each diff side so you can see the on-disk format even when both sides are rendered as canonical JSON:
// GitLineDiff · origin: JSON
{
"openapi": "3.0.0"
}
# GitLineDiff · origin: YAML
{
"openapi": "3.0.0"
}
Set gitlinediff.structuredData.canonicalizeToJson to false to pretty-print
YAML in native YAML syntax instead (JSON files are always rendered as JSON).
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
YAML support is built in. Standalone .yaml/.yml files are parsed as YAML,
pretty-printed, and annotated with an origin badge. When
gitlinediff.structuredData.canonicalizeToJson is enabled (default), YAML is
rendered as canonical JSON so it compares cleanly with .json files carrying
the same data. Embedded JSON strings and YAML block scalars inside YAML config
files are expanded automatically (see Configuration).
To customise extensions or disable cross-format canonicalisation, adjust
gitlinediff.yaml.fileExtensions and
gitlinediff.structuredData.canonicalizeToJson in settings.
For a custom formatter on other extensions, register one explicitly:
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
