@pierre/diffs v1.3.0
Diffs 1.3 is the Edit release. The library that renders your diffs and files can now also edit them: any rendered File, FileDiff, MultiFileDiff, PatchDiff, or CodeView item can be switched into a real, in-place code editor — same DOM, same syntax highlighting, same virtualization — with find & replace, multiple cursors, markers, undo history, and more. Alongside it, this release adds on-demand loading of full file contents for patch-based diffs, added/deleted-file support in more places, custom header/footer regions for CodeView, and a batch of rendering and virtualization bug fixes.
Highlights
- ✏️ Edit mode — turn rendered diffs and files into live editors, in place.
- 📦 New entry point —
@pierre/diffs/edit, loadable only when editing is required. - 🔎 Partial-diff hydration —
loadDiffFilesallows you to fetch full file contents on demand so patch-based diffs can expand their unchanged context. - 🧱 CodeView header & footer regions — build PR summary cards and approval bars inside the scroller.
Breaking changes & upgrade notes
Most apps upgrade with no code changes. Read this section if any of the following applies to you.
1. Firefox support floor raised to 125+
The new edit feature relies on the browser's Intl.Segmenter API (for correct cursor movement across emoji and other multi-part characters), so published builds now target Firefox 125+ (previously 120+). Chrome 123+, Edge 123+, and Safari 17.5+ floors are unchanged — those versions already support Intl.Segmenter.
Migration: nothing to do unless you must support Firefox 120–124. Read-only rendering does not use Intl.Segmenter, and the edit feature-detects it (so importing the edit never throws), but the compiled output as a whole now assumes the newer baseline.
2. A few TypeScript prop types became unions (values unchanged)
To support added-only / deleted-only files, these types changed from interface to union types: MultiFileDiffProps, the SSR types PreloadDiffOptions, PreloadMultiFileDiffOptions, PreloadMultiFileDiffResult, and the vanilla FileDiffRenderProps / FileDiffHydrationProps.
Everything that compiled before still compiles — passing both oldFile and newFile is still valid. The one thing that breaks is extending them as interfaces:
// Before
interface MyProps extends MultiFileDiffProps<MyAnnotation> { ... }
// After — unions can't be extended, intersect instead
type MyProps = MultiFileDiffProps<MyAnnotation> & { ... };3. React CodeView: a few options moved out of the options object
The React CodeView options prop is now properly typed as as CodeViewReactOptions and some old useless props are now properly emitted, including onSelectedLinesChange, createEditor, or controlledSelection.
4. cacheKey has a real contract when persisting editor state
Previously FileContents.cacheKey was only a worker-pool highlight-cache hint. It still is for read-only rendering — but if you enable the editor's persistState option, every editable file must have a unique, stable cacheKey, and the editor throws at attach time if one is missing. When you commit edited contents back into your own state, give the file a fresh cacheKey (the contents changed, so cached highlights for the old key no longer apply).
✏️ Edit mode
The headline feature. Edit mode is experimental in 1.3 — the API may still shift — but it is fully functional and drives the demos at the docs site's /edit page.
What it is
Instead of shipping a separate editor component, Diffs attaches an Editor to the surface you already rendered. The read-only renderer keeps doing what it does (highlighting, virtualization, annotations, themes); the editor takes over input, carets, and selections on top of that DOM. That means:
- Diffs stay diffs. In a
FileDiff, you edit the new-file side and the diff re-tokenizes as you type; deleted lines stay read-only. Unified and split views both work. - Scale carries over. Editing works inside virtualized
CodeView,VirtualizedFile, andVirtualizedFileDiffsurfaces. Off-screen lines render on demand, and an item's editor (including its undo history) survives scrolling out of view and back. - It loads only when you need it. The editor lives in its own
@pierre/diffs/editentry point, meaning if you don't use the feature it should have no added cost.
Enabling it
React — provide an editor factory once, then flip an edit prop:
import { EditProvider, File } from '@pierre/diffs/react';
import { Editor } from '@pierre/diffs/edit';
// Put this provider high up in your layout stack so multiple files/diffs can
// use it
<EditProvider createEditor={(options) => new Editor(options)}>
<File file={file} edit editOptions={{ onChange: handleChange }} />
</EditProvider>;File, FileDiff, MultiFileDiff, and PatchDiff all accept edit and editOptions. For CodeView, set edit: true on the items that should be editable and handle onItemEditChange / onItemEditComplete at the view level; a getEditor(id) method on the ref gives you the item's editor instance.
Vanilla JS — render a component, then attach:
const editor = new Editor({ onChange: handleChange });
const dispose = editor.edit(fileInstance);For vanilla CodeView, pass a createEditor factory in its options; the view owns each editor's lifecycle and reports changes through onItemEditChange / onItemEditComplete.
Saving is yours. The editor never talks to a server. onChange (or the CodeView item callbacks) hands you the complete updated file contents plus the current annotations on every change; committing them to state or backend is your app's job.
What your users get
- Real editing — typing, IME composition (e.g. Japanese/Chinese input), correct handling of emoji and other multi-part characters, mobile input, and an accessible
role="textbox"host. - Multiple cursors — Cmd/Ctrl-click adds carets, Cmd/Ctrl-D selects the next occurrence; one edit applies to every selection.
- Smart indentation — indent/outdent whole selections, with tabs vs. spaces inferred per line; move or copy lines up and down; toggle line and block comments with per-language comment syntax (~30 languages built in, extensible via
languageCommentConfig). - Find & replace — Cmd/Ctrl-F opens a search panel with match-case, whole-word, and regex toggles, a match counter, and replace / replace-all. Regex replace supports capture groups.
- Undo history — a structure-aware undo stack: consecutive typing coalesces into sensible steps, paste/cut stay their own steps, and programmatic edits join the same timeline. Drive it with
editor.undo(),editor.redo(),editor.applyEdits(),canUndo/canRedo. - Markers —
editor.setMarkers()draws severity-colored squiggly underlines (error/warning/info/hint) with hover popovers — ideal for linter or AI feedback. - Selection actions — opt in with
enabledSelectionActionandrenderSelectionAction()to float your own action popover over any text the user selects ("Add to chat", "Copy", …). - Editable line annotations — comment threads and other annotations on the editable side follow the text as lines are added, removed, and merged, and they replay correctly through undo/redo.
- Editing niceties — auto-surround selections with quotes/brackets (
autoSurround), matching-bracket highlighting (matchBrackets), rounded selection rendering, reduced-motion-aware caret. - Collapsed regions behave like folds — the caret skips them, and jumping into one expands just enough to show the line.
- Persisted editor state — with
persistState, a file's document, undo history, selection, and scroll survive the surface rendering a different file and coming back (in-memory or IndexedDB storage; plain file surfaces only). - Custom clipboard — plug in your own
clipboardreader where the native one is restricted, e.g. Electron. - Theme-aware — edits re-tokenize live when you switch themes or toggle light/dark, and SSR-prerendered surfaces hydrate into editable mode without a flash.
Keyboard shortcuts (built in)
| Group | Shortcuts |
|---|---|
| Editing | Tab / Shift-Tab indent/outdent · Alt-↑/↓ move lines · Mod-X/C/V cut/copy/paste · Mod-/ toggle comment
|
| Selection | Arrows + Shift to extend · Mod-←/→ line start/end · Mod-A select all · Mod-Click add cursor · Esc collapse
|
| History | Mod-Z undo · Shift-Mod-Z redo
|
| Find | Mod-F find · Mod-Alt-F replace · Mod-G / Shift-Mod-G next/previous match · Mod-D select next occurrence
|
(Mod = Cmd on macOS, Ctrl elsewhere. Plus copy-line, blank-line insertion,
document start/end motion, and more.)
Known limitations (beta)
- Markers don't automatically move as text changes — re-call
setMarkersafter edits. - The undo stack is bounded (default 100 steps, configurable via
historyMaxEntries). persistStateapplies to file surfaces only (not diffs) and requires stablecacheKeys.- React may recreate annotation content nodes as annotations shift or leave a virtualized window — keep annotation-local state (like comment drafts) keyed by stable IDs outside the rendered node. We plan to fix this in the future.
More new features
- Partial-diff hydration (
loadDiffFiles). Diffs parsed from a patch only contain the lines the patch included, so "expand unchanged context" hits a wall. Pass the newloadDiffFilesasync option (onFileDiffor shared throughCodeView) and the viewer fetches full old/new file contents the first time they're needed, upgrades the diff in place, and unlocks full context expansion. ThehydratePartialDiffandcloneFileDiffMetadatautilities are exported for doing the same manually. Note: hydration mutates thefileDiffobject you passed — keep its identity stable across renders if you want the hydrated state to stick. - Added & deleted files in more places.
MultiFileDiff(React) and the SSR helperspreloadMultiFileDiff/preloadDiffHTMLnow acceptoldFile: null(new file) ornewFile: null(deleted file) instead of requiring both sides. - CodeView header & footer regions.
renderCodeViewHeader/renderCodeViewFooter(React and vanilla) render your own content before the first item and after the last one, inside the scroll container. Heights are measured automatically (async content and font loads included), they render even when the item list is empty, and the host elements are exposed viagetHeaderElement()/getFooterElement()plusdata-diffs-code-view-header/-footerattributes for styling. Ideal for PR summary cards and approval bars. - Filename suffix slot.
renderHeaderFilenameSuffixonFile,FileDiff,MultiFileDiff,PatchDiff, andCodeViewrenders custom content (badges, status chips) immediately after the filename in the file header, joining the existing prefix and metadata slots. - Annotation type guards. New
isFileAnnotation,isDiffAnnotation,isFileAnnotationCollection, andisDiffAnnotationCollectionhelpers narrow the two annotation shapes (file annotations vs. side-tagged diff annotations) in shared handlers. - Virtualization is stable. The virtualization system (
CodeView,Virtualizer,VirtualizedFile,VirtualizedFileDiff) has shed its beta label.
Bug fixes
Tons of bug fixes went into this major release, however we've included a few highlights here:
- Worker-pool startup race. A
CodeViewcreated while the syntax-highlighting worker pool was still initializing could fail to highlight; it now waits for pool readiness, kicks off initialization when needed, and degrades gracefully if the pool fails (#911). VirtualizedFileDiffignored metadata updates. Updating a virtualized diff'sfileDiffafter the initial render now re-resolves and re-renders as expected (#881).- Stale content on recycled rows. Virtualized rows could briefly show a previous file's header or slot content when instances were recycled; fixed, along with reduced layout thrashing during container resizes (#740).
- Leftover spacer nodes after
reset().File/FileDiffreset()left internal top/bottom spacer elements attached to the DOM (#895). - Merge-conflict parsing. Conflict hunks with a zero-count side are now parsed correctly (#978).
- Hunk expansion. Fixed a long-standing
CodeViewbug where expanding collapsed context could misbehave (#861). - React strict mode. Fixed a virtualizer double-mount issue under React strict mode, plus virtualization-invalidation bugs in file rendering (#935).
Full Changelog: diffs-v1.2.12...diffs-v1.3.0