Migrating Editing to Pierre Diffs 1.4
Pierre Diffs 1.4 makes editable components responsible for their active edit session. Applications no longer need to mirror every changed file, diff, or annotation back into the component while the user is typing.
Use component onEditChange to observe or autosave a draft and component onEditComplete as the commit boundary. Return 'accept' to install the completed file or diff and its final annotations, or 'reject' to restore the latest external value and annotations.
API changes
| Before | 1.4 |
|---|---|
new Editor(options?)
| new Editor('file' | 'file-diff', options?, editStateKey?)
|
onChange(file, annotations, event)
| onChange(event)
|
EditorChangeEvent<LAnnotation>
| EditorChangeEvent<'file' | 'file-diff', LAnnotation, Caret>
|
editor.getState() / setState()
| editor.getViewState() / setViewState()
|
EditorState
| EditorViewState
|
Old scroll-only EditorViewState
| EditorViewportState
|
persistState / persistStateStorage
| editStateKey or application-owned storage
|
createEditor(options)
| createEditor(editorType, options, editStateKey)
|
| No editor completion observer | EditorOptions.onComplete(event)
|
cleanUp(recycle?: boolean)
| cleanUp('discard' | 'recycle' | 'complete')
|
The new EditorChangeEvent contains changes, file, lineAnnotations, and the attached editor. For a diff, event.file is the editable new file, not a FileDiffMetadata object.
Prefer component-level onEditChange and onEditComplete callbacks for normal integrations. Their completion return value decides whether to accept or reject. The lower-level EditorOptions.onChange and EditorOptions.onComplete callbacks are for editor-wide observation. You cannot accept or reject from EditorOptions.onComplete. The editor completion observer receives the exact same frozen event before the component callback and still fires when no component completion handler exists. Its argument is EditorEditCompleteEvent<'file' | 'file-diff', LAnnotation, Caret>, the union of FileEditCompleteEvent and FileDiffEditCompleteEvent exported from @pierre/diffs/edit.
Add the EditorType to TypeScript types
Editors are component-specific in 1.4. Use 'file' for File and 'file-diff' for FileDiff. The constructor infers the type when you do not provide generic arguments:
const editor = new Editor('file');If you previously specified annotation metadata, add the editor type before it:
type ThreadMetadata = { threadId: string };
const editor = new Editor<'file-diff', ThreadMetadata>('file-diff');
const options: EditorOptions<'file-diff', ThreadMetadata, undefined> = {};The same editor-type-first order applies to TextDocument, EditState, and EditorInitialState.
Supply metadata arguments to type-only APIs
Type-only APIs such as EditorOptions, EditorFactory, FileOptions, FileDiffOptions, CodeViewOptions, and edit event types require annotation and caret metadata arguments. Use undefined for metadata you do not provide:
type Options = FileDiffOptions<ThreadMetadata, undefined>;
type Change = EditorChangeEvent<'file-diff', ThreadMetadata, undefined>;Concrete classes, React components, and preload functions retain metadata defaults, so they do not require explicit generic arguments.
Use the 'file' | 'file-diff' union only for code that handles both types.
Move editor type imports
Import editor events, state, selections, document positions, markers, focus options, and factory types from @pierre/diffs/edit.
// Before
import type {
EditorChangeEvent,
EditorViewState,
MarkerSeverity,
} from '@pierre/diffs';
// 1.4
import type {
EditorChangeEvent,
EditorViewState,
MarkerSeverity,
} from '@pierre/diffs/edit';Replace structural edit types
Pierre only supports its concrete editor and component classes. The structural types that allowed plain objects to look like supported implementations have been removed:
- Replace
DiffsEditorwithEditor<'file', LAnnotation>orEditor<'file-diff', LAnnotation>. - Replace
DiffsTextDocumentwith the matchingTextDocumenttype. - Replace
DiffsEditableComponent,EditableInstance, andDiffsBaseComponentwith the concreteFileorFileDiffclass being used. - Replace
DiffsComponentOptionswith the options type for the concrete component, such asFileOptions,FileDiffOptions, orCodeViewOptions.
Use a union only when application code genuinely handles multiple component types. Editor.edit() and EditorOptions.onAttach now expose Pierre's matching component instance rather than a separately maintained interface.
Let components own active edits
Remove handlers that immediately feed changed contents or annotations back through render(), React state, or CodeView item updates. The component now owns the draft and remaps annotation positions during the session. Feeding those values back can create loops or overwrite session-owned annotation positions with stale external positions.
Application state remains the external source of truth. Update the file, diff, and annotations once when the edit is accepted:
onEditChange(event)observes the current draft. Use it for autosave, validation, or side effects without updating the active component.File.onEditComplete(event)completes withevent.fileand finalevent.lineAnnotations.FileDiff.onEditComplete(event)completes withevent.fileDiff, final annotations, andoldFile/newFilefor file-pair or patch owners.- Component
onEditCompletealways fires once when the session ends, including unchanged, selection-only, scroll-only, annotation-only, and fully undone sessions. A missing component completion handler rejects.
Vanilla JavaScript
Put lifecycle callbacks on File or FileDiff, then end the session with the disposer returned by editor.edit():
let file = initialFile;
let annotations = initialAnnotations;
let shouldAccept = true;
const view = new File({
onEditChange(event) {
autosaveDraft(event.file);
},
onEditComplete(event) {
if (!shouldAccept) return 'reject';
file = event.file;
annotations = event.lineAnnotations ?? [];
return 'accept';
},
});
view.render({ fileContainer, file, lineAnnotations: annotations });
const editor = new Editor('file', {}, `file:${fileId}`);
const finishEditing = editor.edit(view);
// Save or Cancel sets shouldAccept, then ends the session.
finishEditing();Completed values have no cacheKey. If the surface uses keyed render caching, assign a fresh key before accepting. Do not reuse the replaced value's key and do not clone the file or diff, simply mutate it for the cacheKey.
event.file.cacheKey = `file:${fileId}:${revision}`;
return 'accept';React
File, FileDiff, MultiFileDiff, and PatchDiff expose the same callbacks as top-level props. Move onEditChange and onEditComplete out of the React component's options object. Do not call setFile, setDiff, or setAnnotations from onEditChange; update them at completion:
const cancelRequested = useRef(false);
// Reset to false before starting or saving. Set to true before ending via Cancel.
<File
file={file}
lineAnnotations={annotations}
edit={editing}
editStateKey={`file:${fileId}`}
onEditChange={(event) => autosaveDraft(event.file)}
onEditComplete={(event) => {
if (cancelRequested.current) {
cancelRequested.current = false;
return 'reject';
}
event.file.cacheKey = `file:${fileId}:${nextRevision()}`;
setFile(event.file);
setAnnotations(event.lineAnnotations ?? []);
return 'accept';
}}
/>;Update EditProvider factories to forward all three arguments. The provider no longer reuses editors based on editorOptions object identity.
<EditProvider
createEditor={(editorType, options, editStateKey) =>
new Editor(editorType, options, editStateKey)
}
>
{children}
</EditProvider>When the factory is declared outside JSX, use the exported EditorFactory type so its options and returned editor stay matched to the requested editor type:
import type { EditorFactory } from '@pierre/diffs/edit';
const createEditor: EditorFactory<ThreadMetadata, undefined> = (
editorType,
options,
editStateKey
) => new Editor(editorType, options, editStateKey);CodeView
CodeView callbacks are now event-first. Completion also receives nextItem, which contains the completed value and annotations, edit: false, and an incremented version. onItemEditComplete runs whenever the item session ends, including unchanged, annotation-only, and fully undone sessions. Collapse and virtualization only suspend the session.
const codeView = new CodeView({
getEditStateKey(item) {
return `edit-state:${item.id}`;
},
createEditor(editorType, options, editStateKey) {
return new Editor(editorType, options, editStateKey);
},
onItemEditComplete(event, item, nextItem) {
return 'accept';
},
});The createEditor option remains optional, but a provided factory must always return the editor requested by editorType.
Vanilla CodeView installs nextItem after acceptance. Controlled React CodeView users must also put nextItem into their items state. Rejecting keeps the latest external item. Completion caused by removal or unmount reports the result but cannot reinstall the removed item.
Replace persistState
persistState, persistStateStorage, IStateStorage, and built-in IndexedDB storage have been removed. FileContents.cacheKey now identifies only render/highlighting cache/WorkerPool entries, not edit state.
Use a stable editStateKey to retain the draft, undo/redo history, selections, and eligible scroll state between editor instances in the same session:
<File edit file={file} editStateKey={`draft:${fileId}`} />const editor = new Editor('file', {}, `draft:${fileId}`);For CodeView, return the key from getEditStateKey(item). Keys are explicit application identities, are not inferred from cacheKey, and do not survive a reload.
You can clear an inactive draft after it is submitted or abandoned:
EditStateManager.clear('file', `draft:${fileId}`);Persist across reloads
For local storage or another durable store, save the latest file and optional JSON-safe view state. Not all aspects of state are serializable (like undo history). An example of what is serializable:
function saveDraft(key, event) {
localStorage.setItem(
key,
JSON.stringify({
file: event.file,
viewState: event.editor.getViewState(),
})
);
}
function loadDraft(key) {
const value = localStorage.getItem(key);
if (value == null) return undefined;
try {
return JSON.parse(value);
} catch {
localStorage.removeItem(key);
return undefined;
}
}Pass the loaded file back as the component input. Restore selections and supported scroll offsets with a small initialState:
const draft = loadDraft(`draft:${fileId}`);
<File
file={draft?.file ?? file}
edit
editorOptions={
draft?.viewState
? {
initialState: {
type: 'file',
editor: draft.viewState,
},
}
: undefined
}
/>;This starts with fresh undo history. onEditChange does not fire for selection- or scroll-only changes, so capture getViewState() at an explicit save or navigation boundary when those changes matter. For a durable diff draft, persist the old file and latest new file, then reconstruct the diff after loading.
Other breaking changes
EditHistoryEntryandEditHistoryStatenow take the editor type before annotation metadata. ReplaceEditHistoryLineAnnotation<LAnnotation>withEditorLineAnnotation<'file' | 'file-diff', LAnnotation>, or use the specific editor type when the history belongs to only one component type.- Call the disposer returned by
editor.edit(component)for normal completion.cleanUp('discard')still runs completion callbacks but never installs an accepted result.cleanUp('recycle')only suspends rendering for virtualization and does not complete the session. - Direct component
attachEditor()andcompleteEditSession()calls are no longer supported. Do not call their internal replacements; useeditor.edit(component)and its returned disposer. TextDocument.clone()was removed. Construct a freshTextDocumentfrom its text when fresh history is appropriate.- We no longer automatically derive
cacheKeys, so you must supply them now. Ensure they are always unique based on content.
What's Changed
- [diffs] Improve Invalid Diff Parsing by @amadeus in #1095
- chore: mark React as optional by @onmax in #1091
- [diffs] Cache Key Cleanup by @amadeus in #1055
- [diffs] External vs Internal Edit Hardening by @amadeus in #1072
- [diffs] Edit Completion Handling by @amadeus in #1089
- [diffs] Refactor Edit Persistance by @amadeus in #1092
- [diffs/edit] Misc Webkit Bug Fixes by @amadeus in #1101
- Harden WorkerPool initialzation to better handle errors by @amadeus in #1102
- Fat/editor decorations by @fat in #1103
- fix(diffs): portable lru types by @onmax in #1112
- fix(diffs): avoid stack overflow when rendering massive diffs by @Josh-Walker-GM in #1113
- [diffs/edit] Editor Types Refactor by @amadeus in #1105
- Fat/tab tab tab by @fat in #1104
New Contributors
- @onmax made their first contribution in #1091
- @Josh-Walker-GM made their first contribution in #1113
Full Changelog: diffs-v1.3.6...diffs-v1.4.0