Skip to content
TwinScope0.3.10

Text and code

Three view modes, edited lines paired and marked word by word, long unchanged runs folded, syntax highlighting for nine languages, and a find that does not hide anything.

GIF · press play
The three view modes. The same comparison, re-laid out — no re-run, no scroll reset.

How lines are paired#

A plain line diff reports an edited line as one delete plus one unrelated add, and leaves you to work out that they belong together. TwinScope runs a line pass, then pairs deletes with adds that are similar enough — above a 0.34 similarity threshold — and marks the differing words inside the pair.

You see one modified row, not two mystery rows.

config.ts3 modified · 1 added
42 timeout: 3000,
42 timeout: 10000,
43 retries: 3,
⋯ 214 unchanged lines — click to expand
258export const client = createClient(config);
259export const retryClient = withRetry(client);

View modes#

Cycle with ⌘\, or pick from the toolbar.

ModeWhat it showsBest for
Side by sideBoth versions in aligned columns, as one virtualised list of row pairsReviewing a change
UnifiedOne column, deletes above adds, like git diffNarrow windows, patch review
InlineOne column with word-level marks only, no duplicated rowsProse and copy edits
Click to enlarge

Normalisation toggles#

Three toggles live in the toolbar. Each one re-runs the comparison rather than filtering the view, so the counts in the summary strip always describe what is on screen.

  • Ignore whitespace — leading, trailing and run-length differences stop counting.
  • Ignore case — case-only differences stop counting.
  • Collapse unchanged — runs longer than 8 lines of context fold to 3, expandable in place.
Click to enlarge
Counts come back from the engine, not from the view.

Syntax highlighting#

Nine languages, loaded on demand. Highlighting sits under the change marks and search hits: a hit inside a changed word keeps both, because all three are resolved in one pass rather than layered on top of each other.

src/renderer/src/lib/highlight.tsts
// shiki's JavaScript regex engine — not oniguruma.
// Oniguruma is WASM, and the packaged app loads the renderer from file://,
// where fetching a .wasm is unreliable.
const highlighter = await createHighlighterCore({
engine: createJavaScriptRegexEngine(),
langs: ['typescript', 'json', 'css'],
});

Finding text in a diff#

⌘F is a find, not a filter: nothing disappears. You get a match count, and ⇧⏎ to walk the hits — wrapping at both ends — and Esc to clear.

Search: timeout — 2 / 7
42 timeout: 3000,
42 timeout: 10000,
44 connectTimeout: 500,

A match spanning a mark boundary is still found: searching timeout matches a changed word rendered as ⟦time⟧out, because matching happens against the stripped text and the ranges are re-zipped afterwards.

Limits#

LimitValueWhat happens past it
Line count200,000 per side (400,000 combined)The comparison stops and says so, instead of hanging
Input size over IPC10 MBOnly the path crosses; the worker reads the file itself
Large text input> 10 MBYou are asked before it runs
100k lines, measured195 msOn the owner's Mac, unpackaged build