Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/docs/app/(diffs)/_docs/DocsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
CUSTOM_HUNK_SEPARATORS_SWITCHER,
} from '../docs/CustomHunkSeparators/constants';
import {
EDIT_DECORATION_EXAMPLE,
EDIT_FOCUS_POSITION_EXAMPLE,
EDIT_LAZY_FILE_EXAMPLE,
EDIT_MARKER_EXAMPLE,
Expand Down Expand Up @@ -445,6 +446,7 @@ async function EditSection() {
editSelectionActionExample,
editPersistStateExample,
editPersistStateReactExample,
editDecorationExample,
editMarkerType,
editMarkerExample,
editReactCreateEditorExample,
Expand Down Expand Up @@ -472,6 +474,7 @@ async function EditSection() {
preloadFile(EDIT_SELECTION_ACTION_EXAMPLE),
preloadFile(EDIT_PERSIST_STATE_EXAMPLE),
preloadFile(EDIT_PERSIST_STATE_REACT_EXAMPLE),
preloadFile(EDIT_DECORATION_EXAMPLE),
preloadFile(EDIT_MARKER_TYPE),
preloadFile(EDIT_MARKER_EXAMPLE),
preloadFile(EDIT_REACT_CREATE_EDITOR_EXAMPLE),
Expand Down Expand Up @@ -502,6 +505,7 @@ async function EditSection() {
editSelectionActionExample,
editPersistStateExample,
editPersistStateReactExample,
editDecorationExample,
editMarkerType,
editMarkerExample,
editReactCreateEditorExample,
Expand Down
55 changes: 55 additions & 0 deletions apps/docs/app/(diffs)/_edit/DecorationDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import type { EditorOptions } from '@pierre/diffs/edit';
import { File } from '@pierre/diffs/react';
import type { PreloadedFileResult } from '@pierre/diffs/ssr';
import { useMemo } from 'react';

import {
type CursorDecorationMetadata,
DECORATION_DEMO_DECORATIONS,
} from './constants';

interface DecorationDemoProps {
// Server-preloaded, highlighted File; hydrating from it avoids a highlight flash on load.
prerenderedFile: PreloadedFileResult<undefined>;
}

// Decorations render inside the editor's shadow DOM, so the collaborator
// cursors use inline styles rather than page-level Tailwind classes.
export function DecorationDemo({ prerenderedFile }: DecorationDemoProps) {
const editorOptions = useMemo<
EditorOptions<undefined, CursorDecorationMetadata>
>(
() => ({
renderDecoration({ metadata }) {
const cursor = document.createElement('span');
cursor.ariaLabel = `${metadata.name}'s cursor`;
cursor.style.cssText = `position:relative;display:block;width:2px;height:1lh;background-color:${metadata.color};pointer-events:none;`;

const label = document.createElement('span');
label.ariaHidden = 'true';
label.textContent = metadata.name;
label.style.cssText = `position:absolute;bottom:100%;left:0;padding:1px 5px;border-radius:4px 4px 4px 0;background-color:${metadata.color};color:#fff;font:500 11px/16px system-ui,sans-serif;white-space:nowrap;`;

cursor.append(label);
return cursor;
},
onAttach(editor) {
editor.setDecorations(DECORATION_DEMO_DECORATIONS);
},
}),
[]
);

return (
<div className="not-prose">
<File<undefined, CursorDecorationMetadata>
{...prerenderedFile}
className="diff-container"
edit
editorOptions={editorOptions}
/>
</div>
);
}
21 changes: 21 additions & 0 deletions apps/docs/app/(diffs)/_edit/EditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {

import { WorkerPoolContext } from '../_components/WorkerPoolContext';
import { LiveEditing } from '../_examples/LiveEditing/LiveEditing';
import { DecorationDemo } from './DecorationDemo';
import { EditHero } from './EditHero';
import { EditReference } from './EditReference';
import { FindDemo } from './FindDemo';
Expand All @@ -26,6 +27,7 @@ interface EditPageProps {
historyFile: PreloadedFileResult<undefined>;
keymapFile: PreloadedFileResult<undefined>;
selectionFile: PreloadedFileResult<undefined>;
decorationFile: PreloadedFileResult<undefined>;
}

export function EditPage({
Expand All @@ -36,6 +38,7 @@ export function EditPage({
historyFile,
keymapFile,
selectionFile,
decorationFile,
}: EditPageProps) {
return (
<WorkerPoolContext>
Expand Down Expand Up @@ -68,6 +71,24 @@ export function EditPage({
<SelectionDemo prerenderedFile={selectionFile} />
</div>

<div className="space-y-5">
<FeatureHeader
id="decorations"
title="Decorations"
description={
<>
Use <code>editor.setDecorations()</code> to anchor arbitrary
UI to document positions. Supply typed metadata and map it to
DOM with <code>renderDecoration()</code>—here, custom
decorations render collaborators' cursors. Type or press{' '}
<code>Enter</code> before either cursor to see it follow the
surrounding code.
</>
}
/>
<DecorationDemo prerenderedFile={decorationFile} />
</div>

<div className="space-y-5">
<FeatureHeader
id="markers"
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/app/(diffs)/_edit/KeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
EditorCommand,
EditorKeymap,
EditorShortcut,
KeyboardModifier,
} from '@pierre/diffs/edit';
import { File } from '@pierre/diffs/react';
import type { PreloadedFileResult } from '@pierre/diffs/ssr';
Expand All @@ -21,6 +20,10 @@ interface KeyboardShortcutsProps {
}

type EditorPlatform = NonNullable<EditorKeymap[number]['platform']>;
type ShortcutModifier<T> = T extends `${infer Modifier}+${string}`
? Modifier
: never;
type KeyboardModifier = ShortcutModifier<EditorShortcut>;

interface ShortcutRow {
shortcut: EditorShortcut;
Expand Down
42 changes: 41 additions & 1 deletion apps/docs/app/(diffs)/_edit/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { DEFAULT_THEMES, type FileContents } from '@pierre/diffs';
import {
DEFAULT_THEMES,
type EditorDecoration,
type FileContents,
} from '@pierre/diffs';
import type { EditorCommand, EditorKeymap } from '@pierre/diffs/edit';
import type { FileOptions } from '@pierre/diffs/react';
import type { PreloadFileOptions } from '@pierre/diffs/ssr';
Expand All @@ -12,6 +16,37 @@ const EDITABLE_FILE_OPTIONS: FileOptions<undefined> = {
useTokenTransformer: true,
};

export interface CursorDecorationMetadata {
name: string;
color: string;
}

export const DECORATION_DEMO_FILE: FileContents = {
name: 'review.ts',
contents: `type Review = {
author: string
approved: boolean
}

export function summarize(review: Review) {
const status = review.approved ? 'approved' : 'needs review'
return \`\${review.author}: \${status}\`
}
`,
};

export const DECORATION_DEMO_DECORATIONS: EditorDecoration<CursorDecorationMetadata>[] =
[
{
position: { line: 5, character: 26 },
metadata: { name: 'Amadeus', color: '#7c3aed' },
},
{
position: { line: 7, character: 18 },
metadata: { name: 'Mark', color: '#c2410c' },
},
];

// Lint-marker demo source. Marker positions below are tied to these exact
// lines, so keep the two in sync if the contents change.
export const MARKER_DEMO_FILE: FileContents = {
Expand Down Expand Up @@ -333,6 +368,11 @@ export const MARKER_DEMO_FILE_EXAMPLE: PreloadFileOptions<undefined> = {
options: EDITABLE_FILE_OPTIONS,
};

export const DECORATION_DEMO_FILE_EXAMPLE: PreloadFileOptions<undefined> = {
file: DECORATION_DEMO_FILE,
options: EDITABLE_FILE_OPTIONS,
};

export const FIND_DEMO_FILE_EXAMPLE: PreloadFileOptions<undefined> = {
file: FIND_DEMO_FILE,
options: EDITABLE_FILE_OPTIONS,
Expand Down
67 changes: 64 additions & 3 deletions apps/docs/app/(diffs)/docs/Edit/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,41 @@ editor.setMarkers([]);`,
options,
};

export const EDIT_DECORATION_EXAMPLE: PreloadFileOptions<undefined> = {
file: {
name: 'editor_decorations.ts',
contents: `import { Editor } from '@pierre/diffs/edit';

interface CursorMetadata {
name: string;
color: string;
}

const editor = new Editor<undefined, CursorMetadata>({
renderDecoration({ metadata }) {
const cursor = document.createElement('span');
cursor.ariaLabel = \`\${metadata.name}'s cursor\`;
cursor.style.cssText = \`display:block;width:2px;height:1lh;background:\${metadata.color}\`;
return cursor;
},
onAttach(editor) {
editor.setDecorations([
{
position: { line: 5, character: 26 },
metadata: { name: 'Ada', color: '#7c3aed' },
},
]);
},
});

editor.edit(fileInstance);

// Each call replaces every decoration. Pass an empty array to clear them.
editor.setDecorations([]);`,
},
options,
};

export const EDIT_UNDO_REDO_EXAMPLE: PreloadFileOptions<undefined> = {
file: {
name: 'editor_undo_redo.tsx',
Expand Down Expand Up @@ -1018,11 +1053,12 @@ export const EDITOR_OPTIONS_TYPE: PreloadFileOptions<undefined> = {
} from '@pierre/diffs';
import {
Editor,
type EditorDecoration,
type EditorKeymap,
type IStateStorage,
} from '@pierre/diffs/edit';

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

Expand Down Expand Up @@ -1071,9 +1107,14 @@ interface EditorOptions<LAnnotation> {
// Custom Selection Action UI. See Selection Action docs for context shape.
renderSelectionAction?: (context) => HTMLElement;

// Render custom UI anchored to an EditorDecoration's document position.
renderDecoration?: (
decoration: EditorDecoration<LDecoration>
) => HTMLElement;

// Fires after attach when the text document is ready
onAttach?: (
editor: Editor<LAnnotation>,
editor: Editor<LAnnotation, LDecoration>,
fileInstance: DiffsEditableComponent<LAnnotation>
) => void;

Expand Down Expand Up @@ -1173,6 +1214,10 @@ export const EDITOR_PUBLIC_API: PreloadFileOptions<undefined> = {
} from '@pierre/diffs';
import { Editor, type EditorFocusOptions } from '@pierre/diffs/edit';

interface DecorationMetadata {
label: string;
}

// Editor
// Most methods require an attached surface via edit().

Expand All @@ -1182,7 +1227,13 @@ fileInstance.render({
containerWrapper: document.body,
});

const editor = new Editor();
const editor = new Editor<undefined, DecorationMetadata>({
renderDecoration({ metadata }) {
const element = document.createElement('span');
element.textContent = metadata.label;
return element;
},
});

// Merge partial options at runtime. Existing fields are preserved.
// onChange and similar handlers read from the latest options on each call;
Expand Down Expand Up @@ -1255,6 +1306,16 @@ editor.setMarkers([
]);
editor.setMarkers([]);

// Anchor custom DOM to zero-based document positions. Each call replaces the
// complete decoration set; pass [] to clear. Call after attaching.
editor.setDecorations([
{
position: { line: 1, character: 2 },
metadata: { label: 'Ada' },
},
]);
editor.setDecorations([]);

// Focus the editable content. preventScroll skips scrolling the caret into view.
// Blur removes focus from the content area.
editor.focus();
Expand Down
29 changes: 23 additions & 6 deletions apps/docs/app/(diffs)/docs/Edit/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Edit mode features include:
- History (undo and redo)
- Find-in-file search and replace
- [Selection Action](#edit-mode-selection-action) (opt-in, custom UI)
- Markers (inline diagnostics)
- Markers (inline diagnostics) and custom decorations
- Decorations (arbitrary UI)
- SSR support
- Mobile-friendly
- Lightweight
Expand All @@ -34,8 +35,9 @@ Edit mode is not a full-fledged IDE, though you can build IDE-like experiences
on top of it. It is purpose-built for code rendered by this library: the
existing file and diff surfaces keep their diff layout, annotations, syntax
highlighting, SSR, and virtualization, and edit mode adds editing, multiple
selections, history, search and replace, and markers. That focus makes it a
natural fit for “review and correct” flows with generative code changes.
selections, history, search and replace, markers, and custom decorations. That
focus makes it a natural fit for “review and correct” flows with generative code
changes.

### How It Works

Expand Down Expand Up @@ -99,9 +101,9 @@ selections, and scroll position survive switches — see
Changes to `createEditor` or `editorOptions` do not disturb an active edit
session; their latest values apply the next time `edit` transitions from false
to true. Use `onAttach` with your own ref when controls need imperative APIs
such as history, selections, markers, save, or search. Use `Virtualizer` for
large editable files. The `FileDiff` tab below shows the controlled annotation
feedback loop.
such as history, selections, markers, decorations, save, or search. Use
`Virtualizer` for large editable files. The `FileDiff` tab below shows the
controlled annotation feedback loop.

<EditComponentTabs
fileExample={editReactExample}
Expand Down Expand Up @@ -311,8 +313,23 @@ the `severity` literals against the `Marker` type without importing it.
<DocsCodeExample {...editMarkerExample} />

Markers re-anchor as the document changes, so they stay attached to their text

### Decorations

while you edit. Pass an empty array to clear them.

Decorations anchor arbitrary UI to a zero-based document `position`. Supply
typed `metadata` with each decoration, then map it to an `HTMLElement` with the
`renderDecoration` editor option. The editor owns positioning while your
renderer owns the returned element.

<DocsCodeExample {...editDecorationExample} />

Call `editor.setDecorations(decorations)` after the editor has attached. Like
markers, decorations re-anchor as the document changes. Each call replaces the
complete decoration set; pass an empty array to clear it. The second generic on
`Editor` and `EditorOptions` controls the metadata type.

### History

Each editor keeps a single undo stack per file. Typed input and programmatic
Expand Down
Loading
Loading