Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4e16882
Phase 1: Remove persistState machinery
amadeus Aug 21, 2026
7b5595e
Phase 2.0: First pass at LRU cache for DocumentRegistry
amadeus Aug 21, 2026
f2a38df
Phase 2.1-3: First pass at document-keyed editor state retention
amadeus Aug 24, 2026
902c639
Phase 2.4: Add keyed editor history persistance
amadeus Aug 24, 2026
5af6fde
Phase 3: Broadcast editor state in onEditChange/onEditComplete
amadeus Aug 24, 2026
acd5414
Phase 4: Support state hydration
amadeus Aug 24, 2026
d276b3f
Phase 5: Docs updates
amadeus Aug 24, 2026
0c0b06c
Refactor 1: Replace DocumentRegistry with EditStateManager
amadeus Aug 24, 2026
4bfe5d9
Refactor 2: State capture and restoration
amadeus Aug 25, 2026
83a053e
Refactor 3: Clean up events and how state is emitted
amadeus Aug 25, 2026
28c6445
Refactor 4: Expose and manage complete edit state
amadeus Aug 26, 2026
1c2a945
Refactor 5: Clean up viewport ownership
amadeus Aug 26, 2026
762543b
Refactor 6: Improve tests around new architecture
amadeus Aug 26, 2026
a495ecb
Fix e2e tests with new infra
amadeus Aug 27, 2026
8e34492
Refactor 7: Finalize docs, last minute API improvements for initialState
amadeus Aug 27, 2026
f9a3ac9
fix: fix some docs around cancellation n shit
amadeus Aug 27, 2026
b6b70a0
PR Review Fix: Forcing onEditComplete to ALWAYS fire
amadeus Aug 28, 2026
6bf38b4
Macrodata Refinement: Add `onComplete` event to editor for symmetry
amadeus Aug 28, 2026
caf76f3
codex(review): Fix react based handlers derps
amadeus Aug 28, 2026
4fd50fd
codex(review): Clean up component lifecycle methods
amadeus Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ function renderDiff(parsedPatches: ParsedPatch[], manager?: WorkerPoolManager) {
const patchAnnotations = FAKE_DIFF_LINE_ANNOTATIONS[patchIndex] ?? [];
let hunkIndex = 0;
for (const fileDiff of parsedPatch.files) {
const editor = new Editor<LineCommentMetadata>({
const editor = new Editor<LineCommentMetadata>('file-diff', {
onAttach: (editor) => {
editor.setSelections([
{
Expand Down Expand Up @@ -960,7 +960,7 @@ if (renderFileButton != null) {

virtualizer?.setup(globalThis.document);
const wrap = getWrapped();
const editor = new Editor<LineCommentMetadata>({
const editor = new Editor<LineCommentMetadata>('file', {
enabledSelectionAction: true,
renderSelectionAction: (ctx) => {
const div = document.createElement('div');
Expand All @@ -977,11 +977,11 @@ if (renderFileButton != null) {
div.appendChild(button);
return div;
},
onChange: (file, lineAnnotations) => {
onChange: ({ file, lineAnnotations }) => {
console.log('change', file, lineAnnotations);
},
onAttach: (editor) => {
const { selections } = editor.getState();
const { selections } = editor.getViewState();
if (selections === undefined || selections.length === 0) {
editor.setSelections([
{
Expand Down
16 changes: 4 additions & 12 deletions apps/docs/app/(diffs)/_docs/DocsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ import {
EDIT_ON_ATTACH_REACT_EXAMPLE,
EDIT_ON_ATTACH_VANILLA_EXAMPLE,
EDIT_ON_CHANGE_EXAMPLE,
EDIT_PERSIST_STATE_EXAMPLE,
EDIT_PERSIST_STATE_REACT_EXAMPLE,
EDIT_PERSISTED_DRAFT_EXAMPLE,
EDIT_REACT_CODE_VIEW_EXAMPLE,
EDIT_REACT_CREATE_EDITOR_EXAMPLE,
EDIT_REACT_EXAMPLE,
EDIT_REACT_FILE_DIFF_EXAMPLE,
EDIT_REACT_MULTI_FILE_DIFF_EXAMPLE,
EDIT_REACT_SHARED_EDITOR_EXAMPLE,
EDIT_SELECTION_ACTION_CONTEXT_TYPE,
EDIT_SELECTION_ACTION_EXAMPLE,
EDIT_UNDO_REDO_EXAMPLE,
Expand Down Expand Up @@ -441,18 +439,16 @@ async function EditSection() {
editLazyFileExample,
editorOptionsType,
editOnChangeExample,
editPersistedDraftExample,
editOnAttachReactExample,
editOnAttachVanillaExample,
editFocusPositionExample,
editorPublicApi,
editSelectionActionContextType,
editSelectionActionExample,
editPersistStateExample,
editPersistStateReactExample,
editMarkerType,
editMarkerExample,
editReactCreateEditorExample,
editReactSharedEditorExample,
editReactCodeViewExample,
editReactExample,
editReactFileDiffExample,
Expand All @@ -468,18 +464,16 @@ async function EditSection() {
preloadFile(EDIT_LAZY_FILE_EXAMPLE),
preloadFile(EDITOR_OPTIONS_TYPE),
preloadFile(EDIT_ON_CHANGE_EXAMPLE),
preloadFile(EDIT_PERSISTED_DRAFT_EXAMPLE),
preloadFile(EDIT_ON_ATTACH_REACT_EXAMPLE),
preloadFile(EDIT_ON_ATTACH_VANILLA_EXAMPLE),
preloadFile(EDIT_FOCUS_POSITION_EXAMPLE),
preloadFile(EDITOR_PUBLIC_API),
preloadFile(EDIT_SELECTION_ACTION_CONTEXT_TYPE),
preloadFile(EDIT_SELECTION_ACTION_EXAMPLE),
preloadFile(EDIT_PERSIST_STATE_EXAMPLE),
preloadFile(EDIT_PERSIST_STATE_REACT_EXAMPLE),
preloadFile(EDIT_MARKER_TYPE),
preloadFile(EDIT_MARKER_EXAMPLE),
preloadFile(EDIT_REACT_CREATE_EDITOR_EXAMPLE),
preloadFile(EDIT_REACT_SHARED_EDITOR_EXAMPLE),
preloadFile(EDIT_REACT_CODE_VIEW_EXAMPLE),
preloadFile(EDIT_REACT_EXAMPLE),
preloadFile(EDIT_REACT_FILE_DIFF_EXAMPLE),
Expand All @@ -498,18 +492,16 @@ async function EditSection() {
editLazyFileExample,
editorOptionsType,
editOnChangeExample,
editPersistedDraftExample,
editOnAttachReactExample,
editOnAttachVanillaExample,
editFocusPositionExample,
editorPublicApi,
editSelectionActionContextType,
editSelectionActionExample,
editPersistStateExample,
editPersistStateReactExample,
editMarkerType,
editMarkerExample,
editReactCreateEditorExample,
editReactSharedEditorExample,
editReactCodeViewExample,
editReactExample,
editReactFileDiffExample,
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/(diffs)/_edit/EditReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const CAPABILITY_GROUPS: ReferenceGroup[] = [
{
term: 'Themes & color modes',
description:
'Tokens and editor chrome follow the surface theme, re-tokenizing live when you switch themes or toggle light and dark.',
'Tokens and editor chrome follow the component theme, re-tokenizing live when you switch themes or toggle light and dark.',
},
{
term: 'UI adapts to container',
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/app/(diffs)/_edit/HistoryDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export function HistoryDemo({ prerenderedFile }: HistoryDemoProps) {
[applyEdit]
);

// Build the undo stack once the demo is on screen so the surface arrives
// Build the undo stack once the demo is on screen so the component arrives
// already fully refactored with history intact. We defer until visible
// because seeding scrolls the caret into view and would yank the page down
// to this below-the-fold demo on first load. We poll until the content and
Expand Down Expand Up @@ -314,7 +314,7 @@ export function HistoryDemo({ prerenderedFile }: HistoryDemoProps) {

const startSeeding = () => {
// Warm the shared highlighter before polling so the editor tokenizer can
// pick up the grammar synchronously once the surface attaches.
// pick up the grammar synchronously once the component attaches.
void preloadHighlighter({
themes: [DEFAULT_THEMES.dark, DEFAULT_THEMES.light],
langs: [LANGUAGE],
Expand Down Expand Up @@ -389,7 +389,7 @@ export function HistoryDemo({ prerenderedFile }: HistoryDemoProps) {
// Recover the guided demo after the user typed their own edit. We unwind the
// whole undo stack (the stray edit plus the seeded steps) back to the original
// document via the editor's programmatic `undo()`, which is reliable
// regardless of where focus sits, then replay all seeded edits so the surface
// regardless of where focus sits, then replay all seeded edits so the component
// lands back at the fully-refactored 7/7 state with its history intact.
const reset = useCallback(() => {
const content = getContent();
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/app/(diffs)/_edit/SelectionDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function formatSelectionLineLabel(
// selecting text immediately reveals a floating popover (anchored below the
// selection) whose contents come from `renderSelectionAction`. Here it mimics an
// editor's "Add to chat": the primary action sends the selected snippet to a
// mock chat panel beside the surface, and a secondary action copies it.
// mock chat panel beside the editor, and a secondary action copies it.
export function SelectionDemo({ prerenderedFile }: SelectionDemoProps) {
const [snippets, setSnippets] = useState<ChatSnippet[]>([]);
const snippetIdRef = useRef(0);
Expand Down Expand Up @@ -237,7 +237,7 @@ export function SelectionDemo({ prerenderedFile }: SelectionDemoProps) {
disableLineNumbers: true,
}}
// The page's shared worker pool is wired up for the editable
// editor surface; a dynamically mounted read-only File isn't
// editor component; a dynamically mounted read-only File isn't
// highlighted through it, so highlight on the main thread.
disableWorkerPool
className="max-h-32 overflow-auto"
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/(diffs)/_edit/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { FileOptions } from '@pierre/diffs/react';
import type { PreloadFileOptions } from '@pierre/diffs/ssr';

// The editor requires the token transformer, so enabling it in the SSR preload
// keeps hydration from rerendering the surface after the editor attaches.
// keeps hydration from rerendering the component after the editor attaches.
// Mirrors LiveEditing/constants.ts.
const EDITABLE_FILE_OPTIONS: FileOptions<undefined> = {
theme: DEFAULT_THEMES,
Expand Down
68 changes: 34 additions & 34 deletions apps/docs/app/(diffs)/_examples/LiveEditing/LiveEditing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,39 @@ import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';

interface LiveEditingProps {
// Pre-rendered File surface (the additions-only view) and the FileDiff
// surface (before/after). We ship both so toggling between them hydrates from
// Pre-rendered File component (the additions-only view) and FileDiff component
// (before/after). We ship both so toggling between them hydrates from
// server HTML instead of flashing in after client highlighting.
prerenderedFile: PreloadedFileResult<undefined>;
prerenderedDiff: PreloadFileDiffResult<undefined>;
}

// Which surface the demo renders: a standalone File or a before/after FileDiff.
type Surface = 'file' | 'diff';
// Which component the demo renders: a standalone File or a before/after FileDiff.
type DemoView = 'file' | 'diff';

// Review renders the surface read-only (how diffs renders by default); Edit
// Review renders the component read-only (how diffs renders by default); Edit
// attaches the editor and makes it editable in place.
type EditMode = 'review' | 'edit';

// Layout the diff renders in. Only applies to the FileDiff surface.
// Layout the diff renders in. Only applies to the FileDiff component.
type DiffLayout = 'unified' | 'split';

export function LiveEditing({
prerenderedFile,
prerenderedDiff,
}: LiveEditingProps) {
const [hasEdits, setHasEdits] = useState(false);
const [surface, setSurface] = useState<Surface>('file');
const [view, setView] = useState<DemoView>('file');
// Default to Edit so the editor is live on first paint; the toggle drops back
// to a read-only Review of the same surface.
// to a read-only Review of the same component.
const [mode, setMode] = useState<EditMode>('edit');
// Default to the layout the diff was prerendered in (unified) so the first
// paint hydrates without a flash; toggling re-renders the surface client-side.
// paint hydrates without a flash; toggling re-renders the component client-side.
const [diffLayout, setDiffLayout] = useState<DiffLayout>(
prerenderedDiff.options?.diffStyle === 'split' ? 'split' : 'unified'
);
// Bumping this value remounts the editable surface from pristine input.
// Reset and surface changes use it as a deliberate new-session boundary.
// Bumping this value remounts the editable component from pristine input.
// Reset and view changes use it as a deliberate new-session boundary.
const [resetKey, setResetKey] = useState(0);
// Editing a FileDiff updates the diff metadata it renders from so the live
// hunks stay in sync. Keep an untouched baseline and hand FileDiff a fresh
Expand All @@ -73,40 +73,40 @@ export function LiveEditing({
[pristineFileDiff, resetKey]
);

// Both surfaces synchronously report the current new-file contents.
// Both components synchronously report the current new-file contents.
const handleEditChange = useCallback(
(event: EditorChangeEvent<undefined, 'file' | 'diff'>) => {
setHasEdits(event.file.contents !== LIVE_EDITING_NEW_FILE.contents);
},
[]
);

// Reset and surface switches deliberately discard the current edit session.
// Reset and component switches deliberately discard the current edit session.
// The new key also rebuilds mutable FileDiff metadata from its pristine copy.
const resetEditableSurface = useCallback(() => {
const resetEditableComponent = useCallback(() => {
setHasEdits(false);
setResetKey((key) => key + 1);
}, []);

const handleSurfaceChange = useCallback(
(value: Surface) => {
setSurface(value);
resetEditableSurface();
const handleViewChange = useCallback(
(value: DemoView) => {
setView(value);
resetEditableComponent();
},
[resetEditableSurface]
[resetEditableComponent]
);

// Layout is only a view option, so changing it keeps the current edit session.
const handleDiffLayoutChange = useCallback((value: DiffLayout) => {
setDiffLayout(value);
}, []);

// The Reset button lives in the surface header for both File and FileDiff
// The Reset button lives in the component header for both File and FileDiff
// views, so it's defined once and reused by each `renderHeaderMetadata`.
const renderResetButton = useCallback(
() => (
<button
onClick={resetEditableSurface}
onClick={resetEditableComponent}
disabled={!hasEdits}
title="Revert to the original contents"
className={cn(
Expand All @@ -120,7 +120,7 @@ export function LiveEditing({
Reset
</button>
),
[hasEdits, resetEditableSurface]
[hasEdits, resetEditableComponent]
);

const headerMetadata = mode === 'edit' ? renderResetButton : undefined;
Expand All @@ -133,12 +133,12 @@ export function LiveEditing({
title="Live editing"
description={
<>
Edit mode (experimental) makes any code surface—<code>File</code> or{' '}
<code>FileDiff</code>—editable in place. Toggle between a read-only{' '}
<strong>Review</strong> and a live <strong>Edit</strong>, switch the
surface between a file and a diff, and render the diff unified or
side-by-side split. Start typing in the code below and it updates as
you edit.
Edit mode (experimental) makes any code component—<code>File</code>{' '}
or <code>FileDiff</code>—editable in place. Toggle between a
read-only <strong>Review</strong> and a live <strong>Edit</strong>,
switch the component between a file and a diff, and render the diff
unified or side-by-side split. Start typing in the code below and it
updates as you edit.
</>
}
/>
Expand Down Expand Up @@ -167,9 +167,9 @@ export function LiveEditing({
</div>

<ButtonGroup
value={surface}
onValueChange={(value) => handleSurfaceChange(value as Surface)}
aria-label="Surface"
value={view}
onValueChange={(value) => handleViewChange(value as DemoView)}
aria-label="View"
>
{(['file', 'diff'] as const).map((value) => (
<ButtonGroupItem key={value} value={value} className="capitalize">
Expand All @@ -189,8 +189,8 @@ export function LiveEditing({
key={value}
value={value}
aria-label={value}
// Layout only applies to the diff surface; disable it for files.
disabled={surface === 'file'}
// Layout only applies to the diff component; disable it for files.
disabled={view === 'file'}
>
{value === 'split' ? <IconDiffSplit /> : <IconDiffUnified />}
</ButtonGroupItem>
Expand All @@ -199,7 +199,7 @@ export function LiveEditing({
</div>

<div>
{surface === 'file' ? (
{view === 'file' ? (
<File
key={resetKey}
{...prerenderedFile}
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/(diffs)/_examples/LiveEditing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const LIVE_EDITING_FILE_OPTIONS: FileOptions<undefined> = {

// Server-side preload input for the File view of the Live editing example.
// Spreading the resolved result into <File> ships pre-rendered shadow DOM so
// the initial (default) File surface paints from server HTML instead of
// the initial (default) File component paints from server HTML instead of
// flashing in after client highlighting.
export const LIVE_EDITING_FILE_EXAMPLE: PreloadFileOptions<undefined> = {
file: LIVE_EDITING_NEW_FILE,
Expand Down
Loading