-
Notifications
You must be signed in to change notification settings - Fork 0
Diff Viewer
jvim includes a side-by-side diff viewer for comparing two JSON files with scroll synchronization, fold synchronization, and hunk navigation.
# Compare two JSON files
jvimdiff file1.json file2.json
# Skip JSON normalization (compare raw text)
jvimdiff --no-normalize file1.json file2.json
# Compare JSONL files
jvimdiff --jsonl file1.json file2.jsonAlso available as jvd shortcut.
By default, jvim normalizes both files before comparing:
- Parses JSON and re-serializes with
indent=4andsort_keys=True - This means key ordering differences are ignored
- Only actual value differences are shown
With --no-normalize:
- JSON is formatted with
indent=4but keys are not sorted - Key ordering differences will appear as diffs
- Useful when key order matters to your workflow
For large JSON files with repetitive structure (e.g., arrays of objects), jvim uses a block-based diff optimization:
-
Block detection: Scans both files for repeated
{/[openers at the same indentation level - Minimum threshold: At least 4 blocks at the same indent level are required to activate block mode
-
Segment building: Splits content into segments at block boundaries (opening
{/[to closing}/]) -
Segment matching: Uses
SequenceMatcheron whole segments (joined as strings) instead of individual lines - Intra-segment diff: Within matched-but-different segments, performs line-level diff
This dramatically improves diff quality for files like:
{
"users": [
{ "name": "Alice", "age": 30 },
{ "name": "Bob", "age": 25 },
...hundreds more...
]
}When the total line count (left + right) exceeds 50,000 lines, the diff engine falls back to treating the entire content as a single REPLACE hunk. This prevents excessive computation time on very large files.
For files without sufficient block structure, standard line-by-line SequenceMatcher diff is used.
| Color | Meaning |
|---|---|
Red background (#72261a) |
Deletion — line exists only in the left file |
Green background (#1e5c34) |
Insertion — line exists only in the right file |
Gray background (#6a6a6a) |
Replacement — line differs between files |
Dark background (#2a2a2a) |
Filler — padding line added for alignment |
Filler lines are empty lines inserted on one side to keep the left and right panels aligned vertically.
Both panels scroll and move together automatically:
- The focused panel drives the scroll position, cursor row, and cursor column
- The non-focused panel mirrors
_scroll_top,cursor_row, andcursor_colon every render -
cursor_colis clamped to the target line length to prevent out-of-bounds access - Synchronization happens in real-time, not on discrete events
Folding operations in one panel are mirrored to the other:
-
za(toggle),zo(open),zc(close): Applied to both panels -
zM(fold all),zR(unfold all): Applied to both panels - The fold state is synchronized via
_sync_folds_to_target()API
On initial load:
- All structure is folded (
_fold_all_nested()) - Diff regions are selectively unfolded:
- Any fold containing at least one non-EQUAL line is opened
- Collapsed strings with diff content are expanded
- Both panels receive the same fold state
This shows you only the differences by default, with unchanged sections collapsed.
| Key | Action |
|---|---|
]c |
Jump to next diff hunk (wraps to first hunk after last) |
[c |
Jump to previous diff hunk (wraps to last hunk from first) |
The status bar shows: Hunk N/M (current hunk / total hunks).
If files are identical: "Files are identical".
| Key | Action |
|---|---|
Tab |
Toggle focus between left and right panels |
When focus switches, scroll position is maintained through the sync mechanism. The active panel is indicated by the title bar color — the focused panel has a blue title bar, while the inactive panel has a dimmed gray title bar.
The diff viewer shares the same gutter system as the main editor:
- Left panel: Shows logical line number and JSONL record number (if JSONL)
- Right panel: Shows JSONL record number only (logical line number hidden since both panels have the same line count due to filler rows)
In the diff viewer, ej works across both panels simultaneously:
- Position cursor on a line with an embedded JSON string
- Type
ej— both EJ panels open - The same row is checked in the other panel for embedded JSON
- If both sides have embedded JSON at that row:
- A diff is computed between the two embedded JSONs
- Both EJ panels show the diff with color coding
- If only one side has embedded JSON:
- Only that side's EJ panel opens with the content (no diff coloring)
Like the main editor, the diff viewer supports nested EJ levels:
- Each side maintains its own EJ stack (
_left_ej_stack,_right_ej_stack) - Opening a nested
ejpushes current content to the stack - Closing (
:q) pops the stack and restores the previous level - Both sides' stacks are popped together to maintain synchronization
The EJ panels have their own scroll and fold synchronization, independent of the main panels.
JSONL files are diffed record-by-record:
- Both files are split into records
- Each record is formatted with
indent=4(andsort_keysif normalizing) -
SequenceMatchermatches records as whole units - Matched-but-different records get line-level diff internally
- Records are separated by blank lines in the display
- Blank separator lines carry the diff tag of their surrounding content
JSONL mode for diff is auto-detected when either file has a .jsonl extension. It can also be forced with --jsonl.
The diff viewer is always read-only. Both main panels and EJ panels are created with read_only=True. Navigation, search, and folding work normally, but editing operations are disabled.