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.
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.
View modes#
Cycle with ⌘\, or pick from the toolbar.
| Mode | What it shows | Best for |
|---|---|---|
| Side by side | Both versions in aligned columns, as one virtualised list of row pairs | Reviewing a change |
| Unified | One column, deletes above adds, like git diff | Narrow windows, patch review |
| Inline | One column with word-level marks only, no duplicated rows | Prose and copy edits |
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.
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.
// 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.
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#
| Limit | Value | What happens past it |
|---|---|---|
| Line count | 200,000 per side (400,000 combined) | The comparison stops and says so, instead of hanging |
| Input size over IPC | 10 MB | Only the path crosses; the worker reads the file itself |
| Large text input | > 10 MB | You are asked before it runs |
| 100k lines, measured | 195 ms | On the owner's Mac, unpackaged build |