Skip to content

Diff Viewer

edgar.v edited this page Feb 28, 2026 · 3 revisions

Diff Viewer

jvim includes a side-by-side diff viewer for comparing two JSON files with scroll synchronization, fold synchronization, and hunk navigation.

CLI Usage

# 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.json

Also available as jvd shortcut.

Normalization vs Format

By default, jvim normalizes both files before comparing:

  • Parses JSON and re-serializes with indent=4 and sort_keys=True
  • This means key ordering differences are ignored
  • Only actual value differences are shown

With --no-normalize:

  • JSON is formatted with indent=4 but keys are not sorted
  • Key ordering differences will appear as diffs
  • Useful when key order matters to your workflow

Diff Algorithm

Block-Based Optimization

For large JSON files with repetitive structure (e.g., arrays of objects), jvim uses a block-based diff optimization:

  1. Block detection: Scans both files for repeated {/[ openers at the same indentation level
  2. Minimum threshold: At least 4 blocks at the same indent level are required to activate block mode
  3. Segment building: Splits content into segments at block boundaries (opening {/[ to closing }/])
  4. Segment matching: Uses SequenceMatcher on whole segments (joined as strings) instead of individual lines
  5. 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...
    ]
}

Large File Fallback

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.

Line-Level Diff

For files without sufficient block structure, standard line-by-line SequenceMatcher diff is used.

Color Coding

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.

Scroll & Cursor Synchronization

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, and cursor_col on every render
  • cursor_col is clamped to the target line length to prevent out-of-bounds access
  • Synchronization happens in real-time, not on discrete events

Fold Synchronization

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

Auto-Fold

On initial load:

  1. All structure is folded (_fold_all_nested())
  2. Diff regions are selectively unfolded:
    • Any fold containing at least one non-EQUAL line is opened
    • Collapsed strings with diff content are expanded
  3. Both panels receive the same fold state

This shows you only the differences by default, with unchanged sections collapsed.

Hunk Navigation

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".

Panel Switching

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.

Line Numbers

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)

Embedded JSON Diff (EJ Diff)

In the diff viewer, ej works across both panels simultaneously:

  1. Position cursor on a line with an embedded JSON string
  2. Type ej — both EJ panels open
  3. The same row is checked in the other panel for embedded JSON
  4. 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
  5. If only one side has embedded JSON:
    • Only that side's EJ panel opens with the content (no diff coloring)

Nested EJ Diff Stack

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 ej pushes current content to the stack
  • Closing (:q) pops the stack and restores the previous level
  • Both sides' stacks are popped together to maintain synchronization

EJ Panel Sync

The EJ panels have their own scroll and fold synchronization, independent of the main panels.

JSONL Diff

JSONL files are diffed record-by-record:

  1. Both files are split into records
  2. Each record is formatted with indent=4 (and sort_keys if normalizing)
  3. SequenceMatcher matches records as whole units
  4. Matched-but-different records get line-level diff internally
  5. Records are separated by blank lines in the display
  6. Blank separator lines carry the diff tag of their surrounding content

Auto-Detection

JSONL mode for diff is auto-detected when either file has a .jsonl extension. It can also be forced with --jsonl.

Read-Only Mode

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.


한국어 | Home

Clone this wiki locally