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
76 changes: 64 additions & 12 deletions apps/docs/app/(diffs)/docs/CodeView/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ const readmeFile = {
contents: '# Docs\n\nThis file is rendered inline with the diff list.',
};

const changelogFile = {
name: 'CHANGELOG.md',
contents: '# Changelog\n\n- Added personalized greetings.',
};

export function ReviewSurface() {
const viewerRef = useRef<CodeViewHandle | null>(null);
const [selectedLines, setSelectedLines] =
useState<CodeViewLineSelection | null>(null);

const items = useMemo<CodeViewItem[]>(
const initialItems = useMemo<CodeViewItem[]>(
() => [
{
id: 'diff:src/app.ts',
Expand Down Expand Up @@ -74,9 +79,48 @@ export function ReviewSurface() {
Jump to change
</button>

<button
type="button"
onClick={() => {
const viewer = viewerRef.current;
const item = viewer?.getItem('diff:src/app.ts');
if (item?.type !== 'diff') {
return;
}

viewer.updateItem({
...item,
version: typeof item.version === 'number' ? item.version + 1 : 1,
collapsed: !item.collapsed,
});
}}
>
Toggle app diff
</button>

<button
type="button"
onClick={() => {
const viewer = viewerRef.current;
if (viewer?.getItem('file:CHANGELOG.md') != null) {
return;
}

viewer?.addItems([
{
id: 'file:CHANGELOG.md',
type: 'file',
file: changelogFile,
},
]);
}}
>
Append changelog
</button>

<CodeView
ref={viewerRef}
items={items}
initialItems={initialItems}
style={{ height: 600, overflow: 'auto' }}
options={{
theme: { dark: 'pierre-dark', light: 'pierre-light' },
Expand Down Expand Up @@ -195,17 +239,25 @@ viewer.scrollTo({
behavior: 'smooth-auto',
});

const nextItems = items.map((item) =>
item.id === 'diff:src/app.ts' && item.type === 'diff'
? {
...item,
version: 2,
annotations: [{ side: 'additions', lineNumber: 2 }],
}
: item
);
const appItem = viewer.getItem('diff:src/app.ts');
if (appItem?.type === 'diff') {
viewer.updateItem({
...appItem,
version: 2,
annotations: [{ side: 'additions', lineNumber: 2 }],
});
}

viewer.setItems(nextItems);
viewer.addItems([
{
id: 'file:CHANGELOG.md',
type: 'file',
file: {
name: 'CHANGELOG.md',
contents: '# Changelog\n\n- Added personalized greetings.',
},
},
]);

window.addEventListener('beforeunload', () => {
viewer.cleanUp();
Expand Down
39 changes: 31 additions & 8 deletions apps/docs/app/(diffs)/docs/CodeView/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
can contain files, diffs, or a mix of both.
</Notice>

`CodeView` takes an ordered `CodeViewItem[]` list and manages the hard parts for
you: virtualization, measured layout reconciliation, sticky headers, selection
across items, and `scrollTo` targeting by item, line, or absolute position.
`CodeView` renders an ordered `CodeViewItem[]` list and manages the hard parts
for you: virtualization, measured layout reconciliation, sticky headers,
selection across items, and `scrollTo` targeting by item, line, or absolute
position.

Use it when the amount of content is large, unbounded, or hard to predict. It is
meant to handle anything from a single file up to very large multi-file diff
Expand All @@ -26,7 +27,7 @@ yourself.
### Core Model

- Every item needs a stable unique `id`. That id is how `scrollTo`, selection,
and reconciliation find the correct record.
`getItem`, `updateItem`, and reconciliation find the correct record.
- Items are either `{ type: 'file', file }` or `{ type: 'diff', fileDiff }`.
- If you keep the same item id but change its content or annotations, publish a
new `version` so `CodeView` refreshes that item in place without rebuilding
Expand All @@ -48,21 +49,43 @@ yourself.
vanillaExample={codeViewVanillaExample}
/>

### React Item Ownership

React `CodeView` supports two item ownership models. Use one per mounted viewer;
do not switch between them without remounting with a new `key`.

| Mode | Use | Item prop | Item updates |
| ---------- | -------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
| Controlled | React state owns the complete item list | `items` | Publish a new `items` array. Append-only changes are optimized; other changes reconcile the list. |
| Imperative | The viewer instance owns the item list after mount | optional `initialItems` | Use the ref APIs: `addItems`, `getItem`, and `updateItem`. |

Use controlled mode when item data already lives naturally in React state and
the list is small enough that mutating arrays or items is cheap. Use imperative
mode for very large or streaming surfaces where routing every item update
through React would be expensive. In imperative mode, omit `items`, optionally
seed the viewer with `initialItems`, and use the `CodeViewHandle` to add new
items or update existing ones.

### Usage Notes

- In React, `CodeView` is controlled through props plus an imperative ref.
- In React, pass `items` for controlled item ownership.
- In React, pass `initialItems` instead of `items` for imperative item
ownership. `initialItems` seeds the viewer once; later item changes should go
through the ref.
- In React, `addItems` and `updateItem` require imperative item ownership and
throw if the viewer is controlled with `items`.
- In React, use `selectedLines` and `onSelectedLinesChange` when selection needs
to live in component state.
- In React, use the ref for `scrollTo`, `setSelectedLines`,
`clearSelectedLines`, and `getWindowSpecs`.
- In React, use the ref for `scrollTo`, `setSelectedLines`, `getSelectedLines`,
`clearSelectedLines`, `getItem`, `updateItem`, `addItems`, and `getInstance`.
- `renderCustomHeader`, `renderHeaderPrefix`, `renderHeaderMetadata`,
`renderAnnotation`, and `renderGutterUtility` receive the whole
`CodeViewItem`, which makes it easy to branch on `item.type`.
- In Vanilla JS, `CodeView` owns a scrollable root that you set up once and
update over time.
- In Vanilla JS, call `setup(root)` once with the scrollable container.
- In Vanilla JS, use `setItems`, `addItem`, or `addItems` to populate the
viewer.
viewer, and `getItem` / `updateItem` for item-level imperative changes.
- Shared callbacks receive the normal file/diff payload plus a `context`
argument containing the current viewer item and instance.
- By default, `CodeView` temporarily disables pointer events on rendered content
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/app/(diffs)/docs/ReactAPI/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,9 @@ const readmeFile = {
};

export function ReviewSurface() {
// Pass \`items\` when React owns the full item list. Use \`initialItems\` plus a
// ref instead when item updates should be imperative; omit both item props to
// start empty and append later.
const items = useMemo<CodeViewItem[]>(
() => [
{
Expand Down
8 changes: 5 additions & 3 deletions apps/docs/app/(diffs)/docs/ReactAPI/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ The React API exposes six main components:
input and remount (for example, with a changing `key`) when you want to reset.

The `CodeView` tab above is the quick-start version. For the full guide on
items, ids, `version`, selection, and `scrollTo`, see [CodeView](#code-view).
controlled `items`, imperative `initialItems`, ids, `version`, selection, and
`scrollTo`, see [CodeView](#code-view).

### Shared Props

Expand All @@ -44,8 +45,9 @@ component has similar props, but uses `LineAnnotation` instead of
`DiffLineAnnotation` (no `side` property).

`CodeView` reuses many of the same option names internally, but it has its own
`items`, viewer ref, and mixed-item render props. See [CodeView](#code-view) for
the dedicated guide.
controlled `items` mode, imperative mode with optional `initialItems`, viewer
ref, and mixed-item render props. See [CodeView](#code-view) for the dedicated
guide.

Header customization and collapsing behavior:

Expand Down
20 changes: 20 additions & 0 deletions apps/docs/app/(diffs)/docs/VanillaAPI/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,26 @@ const items: CodeViewItem[] = [

viewer.setItems(items);

const appItem = viewer.getItem('diff:src/app.ts');
if (appItem?.type === 'diff') {
viewer.updateItem({
...appItem,
version: 2,
annotations: [{ side: 'additions', lineNumber: 2 }],
});
}

viewer.addItems([
{
id: 'file:CHANGELOG.md',
type: 'file',
file: {
name: 'CHANGELOG.md',
contents: '# Changelog\n\n- Added personalized greetings.',
},
},
]);

window.addEventListener('beforeunload', () => {
viewer.cleanUp();
});`,
Expand Down
9 changes: 5 additions & 4 deletions apps/docs/app/(diffs)/docs/VanillaAPI/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ theming, and interactivity for you.
(`onMergeConflictResolve` / `onMergeConflictAction`).

The `CodeView` tab above is the quick-start version. For the deeper guide on
`setup`, `setItems`, `version`, selection, and `scrollTo`, see
[CodeView](#code-view).
`setup`, `setItems`, `addItems`, `getItem`, `updateItem`, selection, and
`scrollTo`, see [CodeView](#code-view).

### Props

Expand All @@ -40,8 +40,9 @@ uses `LineAnnotation` instead of `DiffLineAnnotation` (no `side` property).

`CodeView` forwards many of those same options to each rendered item, while
adding viewer-specific controls like `viewerMetrics`, `itemMetrics`,
`stickyHeaders`, `pointerEventsOnScroll`, and `smoothScrollSettings`. See
[CodeView](#code-view) for the dedicated guide.
`stickyHeaders`, `pointerEventsOnScroll`, and `smoothScrollSettings`. Its class
instance also exposes item-level methods such as `addItems`, `getItem`, and
`updateItem`. See [CodeView](#code-view) for the dedicated guide.

Header customization and collapsing behavior:

Expand Down
11 changes: 11 additions & 0 deletions apps/docs/app/(diffshub)/(view)/_components/CodeViewDiffStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { StatItem, StatusRow } from './WorkerPoolStatus';

interface CodeViewDiffStatsProps {
stats: CodeViewDiffStatsData | null;
streaming: boolean;
}

export const CodeViewDiffStats = memo(function CodeViewDiffStats({
stats,
streaming,
}: CodeViewDiffStatsProps) {
const [showStats, setShowStats] = useState(true);

Expand Down Expand Up @@ -40,6 +42,7 @@ export const CodeViewDiffStats = memo(function CodeViewDiffStats({
aria-expanded={showStats}
>
Diff Stats
{streaming && <StreamingIndicator />}
<span className="text-muted-foreground/50">(F2)</span>
</button>
</StatusRow>
Expand Down Expand Up @@ -70,3 +73,11 @@ export const CodeViewDiffStats = memo(function CodeViewDiffStats({
</>
);
});

function StreamingIndicator() {
return (
<span className="rounded-full border border-yellow-500/40 bg-yellow-500/10 px-1.5 py-0.5 text-[10px] leading-none font-medium tracking-wide text-yellow-700 uppercase dark:text-yellow-300">
streaming
</span>
);
}
61 changes: 20 additions & 41 deletions apps/docs/app/(diffshub)/(view)/_components/CodeViewFileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ interface CodeViewFileTreeProps {
// Callback invoked with the underlying tree model once it's mounted, and
// again with `null` on unmount. Lets parents drive imperative APIs like
// search open/close without owning the model creation.
onModelReady?(model: FileTreeModel | null): void;
onSelectItem?(itemId: string): void;
source: CodeViewFileTreeSource | null;
onModelReady(model: FileTreeModel | null): void;
onSelectItem(itemId: string): void;
source: CodeViewFileTreeSource;
}

export const CodeViewFileTree = memo(function CodeViewFileTree({
Expand All @@ -33,43 +33,12 @@ export const CodeViewFileTree = memo(function CodeViewFileTree({
onSelectItem,
source,
}: CodeViewFileTreeProps) {
const previousSourceRef = useRef<CodeViewFileTreeSource | null>(null);
const sourceVersionRef = useRef(0);

if (source == null) {
previousSourceRef.current = null;
return null;
}

if (source !== previousSourceRef.current) {
previousSourceRef.current = source;
sourceVersionRef.current += 1;
}

return (
<CodeViewFileTreeContent
key={sourceVersionRef.current}
className={className}
onModelReady={onModelReady}
onSelectItem={onSelectItem}
source={source}
/>
const sourceRef = useRef(source);
const previousSourceRef = useRef(source);
sourceRef.current = source;
const sort = useStableCallback<CodeViewFileTreeSource['sort']>(
(left, right) => sourceRef.current.sort(left, right)
);
});

interface CodeViewFileTreeContentProps extends Omit<
CodeViewFileTreeProps,
'source'
> {
source: CodeViewFileTreeSource;
}

function CodeViewFileTreeContent({
className,
onModelReady,
onSelectItem,
source,
}: CodeViewFileTreeContentProps) {
const onSelectionChange = useStableCallback(
(selectedPaths: readonly FileTreePublicId[]) => {
if (selectedPaths.length !== 1 || onSelectItem == null) {
Expand All @@ -87,11 +56,21 @@ function CodeViewFileTreeContent({
...BASE_FILE_TREE_OPTIONS,
gitStatus: source.gitStatus,
paths: source.paths,
sort: source.sort,
sort,
onSelectionChange,
itemHeight: 24,
});

useEffect(() => {
if (previousSourceRef.current === source) {
return;
}

previousSourceRef.current = source;
model.resetPaths(source.paths);
model.setGitStatus(source.gitStatus);
}, [model, source]);

useEffect(() => {
onModelReady?.(model);
return () => {
Expand All @@ -109,4 +88,4 @@ function CodeViewFileTreeContent({
style={DENSITY_OVERRIDE_STYLES}
/>
);
}
});
Loading
Loading