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
22 changes: 6 additions & 16 deletions packages/diffs/src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,12 +1336,7 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
}

// Drop the DOM-geometry caches (memoized row elements, measured line Ys,
// last caret x). Wrap offsets are intentionally NOT cleared here: they
// depend only on line text, font metrics, and content width — not on the
// rendered rows — so they stay valid across render-range syncs and row
// rebuilds. The sites where those inputs actually change (text edits in
// #applyChange, width changes in #handleLayoutResize, font remeasure,
// document swaps, cleanUp) invalidate the wrap cache themselves.
// last caret x).
#resetCache(): void {
this.#lineYCache.clear();
this.#lineElementsCache.clear();
Expand Down Expand Up @@ -3158,16 +3153,11 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
);
}

// A diff re-renders its rows in place after the edits above: a unified diff
// rebuilds its content column on every edit (FileDiff.refreshDiffView swaps
// the column's innerHTML), and any diff rebuilds on a line-count change
// (applyDocumentChange -> full render). That detaches the line elements this
// editor memoized for caret/selection geometry (#lineYCache,
// #lastAccessedLineElement), so a detached row would measure offsetTop 0 and
// the caret would render at the top - the following #scrollToPrimaryCaret
// then scrolls the viewport there. Drop the geometry caches so the overlay
// re-measures against the freshly rebuilt rows and the caret stays put.
if (this.#isDiff && (this.#diffSyle === 'unified' || didLineCountChange)) {
// A line-count change can remove cached rows, while a unified diff rebuilds
// its content column on every edit. Either can detach the line elements
// memoized for caret/selection geometry, making offsetTop read as 0 and
// scrolling the caret to the top. Re-measure against the current rows.
if (didLineCountChange || (this.#isDiff && this.#diffSyle === 'unified')) {
this.#resetCache();
}

Expand Down
62 changes: 62 additions & 0 deletions packages/diffs/test/editorVirtualizedEdit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,68 @@ async function createWindowedEditor(
}

describe('Editor edits at the bottom of a virtualized window', () => {
test('keeps the last-line caret anchored after Enter, Delete, Enter', async () => {
const { cleanup, content, editor, fileContainer } =
await createWindowedEditor(1_000, makeRange(950, 50));
try {
Object.defineProperty(window.HTMLElement.prototype, 'offsetTop', {
configurable: true,
get(this: HTMLElement) {
const line = Number(this.dataset.line);
return this.isConnected && line > 0 ? line * 20 : 0;
},
});

editor.setSelections([
{
start: { line: 999, character: 9 },
end: { line: 999, character: 9 },
direction: 'none',
},
]);
content.dispatchEvent(
new window.InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
composed: true,
inputType: 'insertParagraph',
})
);
await wait(0);
const firstCaretTransform =
fileContainer.shadowRoot?.querySelector<HTMLElement>('[data-caret]')
?.style.transform;
expect(firstCaretTransform).toBeDefined();

content.dispatchEvent(
new window.InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
composed: true,
inputType: 'deleteContentBackward',
})
);
await wait(0);
content.dispatchEvent(
new window.InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
composed: true,
inputType: 'insertParagraph',
})
);
await wait(0);

expect(editor.getState().selections?.at(-1)?.start.line).toBe(1_000);
expect(
fileContainer.shadowRoot?.querySelector<HTMLElement>('[data-caret]')
?.style.transform
).toBe(firstCaretTransform);
} finally {
cleanup();
}
});

// A run of Enter presses at the window's bottom edge must keep rendering each
// new line. The editor widens its render range when the caret reaches the
// last rendered line; if that widened range is not persisted, the next edit
Expand Down