Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 4 additions & 5 deletions apps/docs/app/(diffs)/docs/CoreTypes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ interface FileContents {
// See: https://shiki.style/languages
lang?: SupportedLanguages;

// Optional: Cache key for AST caching in Worker Pool.
// When provided, rendered AST results are cached and reused.
// IMPORTANT: The key must change whenever the content, filename,
// lang, or revision changes!
// Optional identity for Worker Pool caching. Required when
// Editor.persistState is enabled; use a unique, stable key for that editing
// session and reuse it only when the cached document should resume.
cacheKey?: string;
}

Expand All @@ -39,7 +38,7 @@ const file: FileContents = {
// We'll attempt to detect the language based on file extension
name: 'example.tsx',
contents: 'export function Hello() { return <div>Hello</div>; }',
cacheKey: 'example-file-v1', // Must change if contents change
cacheKey: 'example-file-v1',
};

// With explicit language override
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/app/(diffs)/docs/CoreTypes/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ does not exist. Empty file contents are still a real file; represent them with

<DocsCodeExample {...fileContentsType} />

For read-only rendering and Worker Pool caching, `cacheKey` is optional; when
provided, treat it as a revision identity and change it with the contents,
filename, language, or revision. When
[`Editor.persistState`](#edit-mode-persisting-file-state) is enabled, every
editable file requires an explicit, non-empty `cacheKey`. Use unique, stable
keys for editing sessions, and change the key when incoming contents should
replace the cached document.

### FileDiffMetadata

`FileDiffMetadata` represents the differences between file versions. It contains
Expand Down
12 changes: 11 additions & 1 deletion apps/docs/app/(diffs)/docs/Editor/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,12 +677,22 @@ export const EDITOR_OPTIONS_TYPE: PreloadFileOptions<undefined> = {
DiffsEditableComponent,
FileContents,
} from '@pierre/diffs';
import { Editor } from '@pierre/diffs/editor';
import { Editor, type IStateStorage } from '@pierre/diffs/editor';

interface EditorOptions<LAnnotation> {
// Max undo stack entries
historyMaxEntries?: number;

// Preserve each File's document and editor state between renders.
// Requires every editable file to provide a unique, stable cacheKey.
// Default: false.
persistState?: boolean;

// Where serializable editor state is stored. Text documents and undo
// history remain in this Editor instance's in-memory cache.
// Defaults to 'inMemory' when persistState is enabled.
persistStateStorage?: 'inMemory' | 'indexedDB' | IStateStorage;

// Render rounded corners on selection ranges (default: true)
roundedSelection?: boolean;

Expand Down
52 changes: 52 additions & 0 deletions apps/docs/app/(diffs)/docs/Editor/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,58 @@ Pass these when constructing `new Editor({ ... })`, or update them later with

<DocsCodeExample {...editorOptionsType} />

#### Persisting file state

State persistence is disabled by default. Set `persistState: true` to preserve
an editable `File`'s text document, undo history, selections, and scroll
position when the attached file renders a different file and later returns. This
currently applies to `File` and `VirtualizedFile` surfaces; diff surfaces do not
use the persistence cache.

Every editable file must provide an explicit, non-empty `file.cacheKey` while
`persistState` is enabled. The editor throws if it encounters an unkeyed file;
it does not fall back to `file.name`. Cache keys must be unique and stable
within the persistence storage namespace. Reuse the same key when a rename or
move should resume the same editing session; change it when new incoming
contents should start a fresh document.

```ts
import type { EditorState, FileContents } from '@pierre/diffs';
import { Editor, type IStateStorage } from '@pierre/diffs/editor';

const editor = new Editor({
persistState: true,
persistStateStorage: 'inMemory',
});

const file: FileContents = {
name: 'src/example.ts',
contents: 'export const value = 1;',
cacheKey: 'workspace-file-1',
};

// Custom storage may be synchronous or asynchronous.
const states = new Map<string, EditorState>();
const customStorage: IStateStorage = {
get(cacheKey) {
return states.get(cacheKey);
},
set(cacheKey, state) {
states.set(cacheKey, state);
},
};

const editorWithCustomStorage = new Editor({
persistState: true,
persistStateStorage: customStorage,
});
```

`persistStateStorage` accepts `'inMemory'`, `'indexedDB'`, or an `IStateStorage`
implementation. It defaults to `'inMemory'`. Text documents and undo history
remain scoped to the `Editor` instance; IndexedDB and custom storage persist
only the serializable editor state (`selections` and `view`).

### API Reference

These methods are available on an `Editor` instance. Attach it to a rendered
Expand Down
15 changes: 13 additions & 2 deletions apps/docs/app/(trees)/_components/DemoTreeApp.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileContents } from '@pierre/diffs';
import { preloadFile } from '@pierre/diffs/ssr';
import { FILE_TREE_DENSITY_PRESETS } from '@pierre/trees';
import { preloadFileTree } from '@pierre/trees/ssr';
Expand Down Expand Up @@ -30,6 +31,16 @@ const TREE_APP_LIGHT_FILE_OPTIONS = {
themeType: 'light',
} as const;

// Initial paths are unique and survive moves because every file remap spreads
// the existing value. Use them as stable editor identities for this demo.
const TREE_APP_EDITOR_FILES: Readonly<Record<string, FileContents>> =
Object.fromEntries(
Object.entries(TREE_APP_DEMO_FILES).map(
([path, file]) =>
[path, { ...file, cacheKey: file.cacheKey ?? path }] as const
)
);

export async function DemoTreeApp() {
const treePreloadedData = preloadFileTree({
dragAndDrop: true,
Expand All @@ -54,7 +65,7 @@ export async function DemoTreeApp() {
// fall back to an on-the-fly highlighter pass. Each file produces two
// results, so we run them all in a single Promise.all to minimize latency.
const preloadedEntries = await Promise.all(
Object.entries(TREE_APP_DEMO_FILES).map(async ([path, file]) => {
Object.entries(TREE_APP_EDITOR_FILES).map(async ([path, file]) => {
const [darkResult, lightResult] = await Promise.all([
preloadFile({ file, options: TREE_APP_DARK_FILE_OPTIONS }),
preloadFile({ file, options: TREE_APP_LIGHT_FILE_OPTIONS }),
Expand All @@ -81,7 +92,7 @@ export async function DemoTreeApp() {

return (
<DemoTreeAppClient
files={TREE_APP_DEMO_FILES}
files={TREE_APP_EDITOR_FILES}
initialActivePath={TREE_APP_DEMO_INITIAL_ACTIVE_PATH}
initialExpandedPaths={TREE_APP_DEMO_INITIAL_EXPANDED_PATHS}
paths={TREE_APP_DEMO_PATHS}
Expand Down
Loading