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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const LIVE_DIFF_EDITOR_EXAMPLE: PreloadFileDiffOptions<undefined> = {
useTokenTransformer: true,
enableGutterUtility: false,
enableLineSelection: false,
expandUnchanged: true,
lineHoverHighlight: 'disabled',
},
};
3 changes: 1 addition & 2 deletions apps/docs/app/(diffs)/_examples/LiveEditor/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const LIVE_EDITOR_OPTIONS: MultiFileDiffProps<undefined>['options'] = {
useTokenTransformer: true,
enableGutterUtility: false,
enableLineSelection: false,
expandUnchanged: true,
lineHoverHighlight: 'disabled',
};

Expand All @@ -97,7 +96,7 @@ export const LIVE_EDITOR_EXAMPLE: PreloadMultiFileDiffOptions<undefined> = {
// File-mode options for the Live editing example. They mirror the editor's
// enforced contentEditable state (see LIVE_EDITOR_OPTIONS) so the SSR-rendered
// File matches what the editor attaches to, avoiding a rerender flash on
// hydration. Diff-only keys (diffStyle/expandUnchanged) don't apply to a File.
// hydration. The diff-only diffStyle key doesn't apply to a File.
export const LIVE_EDITOR_FILE_OPTIONS: FileOptions<undefined> = {
theme: DEFAULT_THEMES,
themeType: 'dark',
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/app/(diffs)/_home/mockData.generated.ts

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions apps/docs/app/(diffs)/_home/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import type { GitStatus, GitStatusEntry } from '@pierre/trees';
import { GENERATED_AUI_SESSIONS } from './mockData.generated';

// Render options shared by the agent demo's SSR preload (Home.tsx) and its
// client FileDiff (AgentUi). Beyond the visual options, these bake in the exact
// state the editor enforces when it attaches to an editable FileDiff: the token
// transformer on, gutter utility and line selection off, every unchanged line
// expanded, and line-hover highlighting disabled. The editor only re-renders an
// attached surface when these aren't already set, so if the prerendered markup
// omits them (especially `expandUnchanged` and `useTokenTransformer`) the
// hydrated DOM no longer matches the editor's line model — which mis-positions
// the caret/selection and breaks editing. Sharing one constant also keeps the
// server and client diffStyle in lockstep so the prerendered HTML always
// matches what the client renders.
// client FileDiff (AgentUi). Beyond the visual options, these bake in the
// exact state the editor enforces when it attaches to an editable FileDiff:
// the token transformer on, gutter utility and line selection off, and
// line-hover highlighting disabled. The editor only re-renders an attached
// surface when these aren't already set, so if the prerendered markup omits
// them (especially `useTokenTransformer`) the hydrated DOM no longer matches
// the editor's line model — which mis-positions the caret/selection and
// breaks editing. Sharing one constant also keeps the server and client
// diffStyle in lockstep so the prerendered HTML always matches what the
// client renders.
export const AUI_DIFF_OPTIONS: DiffBasePropsReact<undefined>['options'] = {
theme: DEFAULT_THEMES,
themeType: 'dark',
Expand All @@ -28,7 +28,6 @@ export const AUI_DIFF_OPTIONS: DiffBasePropsReact<undefined>['options'] = {
useTokenTransformer: true,
enableGutterUtility: false,
enableLineSelection: false,
expandUnchanged: true,
lineHoverHighlight: 'disabled',
};

Expand Down
13 changes: 8 additions & 5 deletions apps/docs/app/(diffs)/docs/Editor/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@ You render the surface first, then attach editing:
annotations when present).

When attaching, edit mode normalizes component options that conflict with
editing and triggers a re-render: it turns on the token transformer, turns off
gutter utilities, line selection, and line-hover highlighting, and expands any
collapsed unchanged regions. The diff layout is preserved, so a `FileDiff` or
`MultiFileDiff` stays in whatever view it was rendered in; in unified diffs,
deleted lines and annotation lines remain read-only.
editing and triggers a re-render: it turns on the token transformer and turns
off gutter utilities, line selection, and line-hover highlighting. Collapsed
unchanged regions stay collapsed: arrow keys skip over them like code folds,
jumps that must land inside one (search matches, undo, Cmd/Ctrl+End) expand it
just enough to show the target, and the separator expand buttons keep working.
The diff layout is preserved, so a `FileDiff` or `MultiFileDiff` stays in
whatever view it was rendered in; in unified diffs, deleted lines and annotation
lines remain read-only.

### React

Expand Down
83 changes: 82 additions & 1 deletion apps/docs/app/(diffs)/playground/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,71 @@ const user = await getUser('123');
\`\`\`
`;

// Nested markup with meaningful indentation, for exercising edit-mode diff
// alignment: wrapping/unwrapping containers, pushing lines around with
// Enter, and re-indenting all reshape change blocks whose lines differ only
// (or mostly) in whitespace. The unchanged middle keeps a collapsible gap
// between the changed regions.
const OLD_MARKUP_CONTENT = `<section class="profile">
<header class="profile-header">
<div class="avatar-wrap">
<img src="/avatars/ada.png" alt="Ada Lovelace" />
</div>
<h2 class="profile-name">Ada Lovelace</h2>
</header>
<div class="profile-body">
<p class="bio">Mathematician and writer.</p>
<ul class="links">
<li>
<a href="/notes">Notes</a>
</li>
<li>
<a href="/programs">Programs</a>
</li>
<li>
<a href="/letters">Letters</a>
</li>
</ul>
<div class="stats">
<span class="stat">12 notes</span>
<span class="stat">3 programs</span>
</div>
</div>
<footer class="profile-footer">
<button class="follow">Follow</button>
</footer>
</section>
`;

const NEW_MARKUP_CONTENT = `<section class="profile profile--wide">
<header class="profile-header">
<img src="/avatars/ada.png" alt="Ada Lovelace" />
<h2 class="profile-name">Ada Lovelace</h2>
</header>
<div class="profile-body">
<p class="bio">Mathematician and writer.</p>
<ul class="links">
<li>
<a href="/notes">Notes</a>
</li>
<li>
<a href="/programs">Programs</a>
</li>
<li>
<a href="/letters">Letters</a>
</li>
</ul>
<div class="stats">
<span class="stat">12 notes</span>
<span class="stat">3 programs</span>
</div>
</div>
<footer class="profile-footer">
<button class="follow" type="button">Follow</button>
</footer>
</section>
`;

// The base files are replicated into several uniquely-named variants so the
// Virtualizer and CodeView demos have enough content to scroll through. Each
// variant is a full (non-partial) diff parsed from complete old/new contents.
Expand Down Expand Up @@ -287,7 +352,18 @@ const README_BASE: BaseDiff = {
newContents: NEW_README_CONTENT,
};

const BASE_DIFFS: BaseDiff[] = [USERS_BASE, STYLES_BASE, README_BASE];
const MARKUP_BASE: BaseDiff = {
name: 'ui/profile-card.html',
oldContents: OLD_MARKUP_CONTENT,
newContents: NEW_MARKUP_CONTENT,
};

const BASE_DIFFS: BaseDiff[] = [
USERS_BASE,
MARKUP_BASE,
STYLES_BASE,
README_BASE,
];

// Appends a variant index before the file extension (e.g. `users.ts` ->
// `users-2.ts`) so each replicated file has a distinct name and id.
Expand Down Expand Up @@ -329,6 +405,11 @@ export const CODE_VIEW_ITEMS: CodeViewItem<PlaygroundAnnotationMetadata>[] =
type: 'diff',
fileDiff: variantDiff(USERS_BASE, index),
},
{
id: `diff:${variantName(MARKUP_BASE.name, index)}`,
type: 'diff',
fileDiff: variantDiff(MARKUP_BASE, index),
},
{
id: `file:${readmeName}`,
type: 'file',
Expand Down
27 changes: 19 additions & 8 deletions packages/diffs/src/components/CodeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { areSelectionsEqual } from '../utils/areSelectionsEqual';
import { areThemesEqual } from '../utils/areThemesEqual';
import { createCodeViewHeaderFooterHostElement } from '../utils/createCodeViewHeaderFooterHostElement';
import { createWindowFromScrollPosition } from '../utils/createWindowFromScrollPosition';
import { finishEditSessionForDiff } from '../utils/editSessionHunks';
import { isStyleNode } from '../utils/isStyleNode';
import { prefersReducedMotion } from '../utils/prefersReducedMotion';
import { roundToDevicePixel } from '../utils/roundToDevicePixel';
Expand Down Expand Up @@ -332,7 +333,6 @@ const CODE_VIEW_EDIT_FORCED_OPTION_KEYS: ReadonlySet<string> = new Set([
'enableGutterUtility',
'enableLineSelection',
'lineHoverHighlight',
'expandUnchanged',
]);

type CodeViewPassThroughOptions<LAnnotation> = Pick<
Expand Down Expand Up @@ -2098,6 +2098,24 @@ export class CodeView<LAnnotation = undefined> {
record.editor.cleanUp();
this.itemEditors.delete(id);
this.attachedEditors.delete(id);
// When the session's instance was released by virtualization, the
// cleanUp above had no detach closure left to run the exit recompute,
// so finish the session here (idempotent: the dirty marker clears on
// the first run). A live item goes through its instance, which also
// preserves expansion state and invalidates layout; removed items fall
// back to a plain metadata recompute from the last change's snapshot.
const itemSnapshot = item?.item ?? record.state.lastChange?.item;
if (itemSnapshot?.type === 'diff') {
if (
item != null &&
item.type === 'diff' &&
item.instance.completeEditSession()
) {
this.markItemLayoutDirty(item);
this.render();
}
finishEditSessionForDiff(itemSnapshot.fileDiff);
Comment thread
amadeus marked this conversation as resolved.
}
const { lastChange } = record.state;
if (lastChange != null) {
// Prefer the current item record (it carries the update that ended
Expand Down Expand Up @@ -2217,8 +2235,6 @@ export class CodeView<LAnnotation = undefined> {
// Edit-forced options: while the item is in edit mode these serve the
// values Editor.edit requires so it never falls back to
// instance.setOptions (which throws for CodeView-managed instances).
// Diffs additionally require expandUnchanged so every editable line of
// the new file is renderable.
defineItemOption(prototype, 'useTokenTransformer', (receiver) =>
this.isReceiverEdited(receiver, 'diff')
? true
Expand All @@ -2239,11 +2255,6 @@ export class CodeView<LAnnotation = undefined> {
? 'disabled'
: this.options.lineHoverHighlight
);
defineItemOption(prototype, 'expandUnchanged', (receiver) =>
this.isReceiverEdited(receiver, 'diff')
? true
: this.options.expandUnchanged
);

defineItemOption(
prototype,
Expand Down
Loading