Skip to content

[diffs/edit] Programmatic edits join the undo timeline like local edits#987

Merged
ije merged 5 commits into
beta-1.3from
fat/history-equivalence
Jul 15, 2026
Merged

[diffs/edit] Programmatic edits join the undo timeline like local edits#987
ije merged 5 commits into
beta-1.3from
fat/history-equivalence

Conversation

@fat

@fat fat commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Fixes the largest bug cluster from the editor-behavior audit (7 pinned bugs, one root cause): history entries were frozen while applyEdits(..., updateHistory=false) bypassed the EditStack, so any programmatic edit left the stack holding stale offsets — undo corrupted text, redo replayed at wrong positions, and undo-to-exhaustion could never restore the original document.

Design decision

Programmatic edits are not a separate semantic class: a mixed programmatic/local sequence must behave exactly like the same sequence applied all-locally. Concretely, after any sequence, undo-to-exhaustion restores the original text and redo-to-exhaustion the final text, with per-step states matching the all-local run.

// before: undo corrupts ('ilot?' …); after:
doc.applyEdits(insert('!'), true);   // local  -> "base!"
doc.applyEdits(insert('>> '));       // programmatic -> ">> base!"
undoAll(doc);                        // ">> base!" -> "base!" -> "base"
redoAll(doc);                        // -> ">> base!?" — byte-exact both ways

How

  • TextDocument.applyResolvedEdits always creates/coalesces/pushes an EditStack entry; updateHistory now only gates caller-supplied selection + undo-boundary metadata on the entry. Redo clears on every recorded edit, tracked or not.
  • Editor.applyEdits always tracks; its updateHistory param is kept for API compatibility but deprecated.
  • No recursion / no bypass leak: undo()/redo() always wrote through the private #applyResolvedEditsToBuffer and are untouched; that private method is the only non-tracking door. History reset remains document rebuild with a fresh stack.
  • Composition unaffected: previews never touch the document (browser composes in DOM; compositionend commits once through the tracked path) — verified end-to-end in review.

Tests

  • The 7 equivalence pins flip to permanent regression tests with assertions unchanged — they encode all-tracked reference runs, independently re-derived by a snapshot-based simulator during review (25/25 values matched).
  • The 2 old-model companion pins are rewritten to the new contract; exactly one other test in the suite pinned the bypass (applyEdits default does not record undo) and is flipped with a cross-reference.
  • Docs updated (applyEdits example + History section) — they described the old flag.
  • Full diffs suite: 1278 pass / 0 fail (run twice, identical); typecheck + oxlint clean. README known-bug inventory: 32 → 25.

Review notes

The riskiest surfaces were checked specifically: recursive recording via undo/redo replay, bypass leaking publicly, composition previews entering history, redo-clear semantics on the coalesce path, metadata-less entries in selection restore, and version-counter drift — all clean. Second PR of the failing-test burn-down (after #986).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Jul 15, 2026 5:07pm
pierre-docs-diffshub Ready Ready Preview Jul 15, 2026 5:07pm
pierre-docs-trees Ready Ready Preview Jul 15, 2026 5:07pm
pierrejs-diff-demo Ready Ready Preview Jul 15, 2026 5:07pm

Request Review

Design decision: edits applied through applyEdits are not a separate
semantic class — a mixed programmatic/local sequence must behave exactly
like the all-local sequence. Previously updateHistory=false skipped the
EditStack entirely, so history entries carried stale offsets after any
programmatic edit: undo could corrupt text, redo replayed at wrong
positions, and exhaustion never restored the original document.

TextDocument.applyResolvedEdits now always creates/coalesces/pushes an
entry; the updateHistory flag only gates caller-supplied selection and
undo-boundary metadata. Editor.applyEdits always tracks (its flag is
kept but deprecated). Undo/redo replay is untouched — it always wrote
through the private buffer path, so no recursion. Composition previews
never touched the document and are unaffected.

Flips the seven history-equivalence pins to permanent regression tests
(assertions unchanged — they encode the all-tracked reference), rewrites
the two old-model pins, and updates the one bypass-model test elsewhere
in the suite plus the public docs. Pinned known-bug count: 32 -> 25.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-commit formatter globs cover js/ts but not .mdx, so the History
section edit landed unformatted and failed root:format-check in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ije

ije commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97812b4aed

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +269 to +270
updateHistory ? selectionsBefore : undefined,
updateHistory ? selectionsAfter : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep editor selections coherent for false-history undo

When Editor.applyEdits(..., false) is used while the editor has a caret, this now creates an undoable entry but strips both selection snapshots here. Undo/redo then returns undefined selections, so Editor.#applyChange leaves #selections in the coordinates from the other document version; for example inserting a line above the caret with updateHistory=false remaps the caret down, but undoing the now-undoable edit leaves it on the non-existent lower line and the next input/focus can act on stale coordinates. Since this commit makes these edits user-undoable, the editor still needs some selection remap/restoration path even when caller metadata is omitted.

Useful? React with 👍 / 👎.

@ije ije left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ije ije merged commit bce11a6 into beta-1.3 Jul 15, 2026
8 checks passed
@ije ije deleted the fat/history-equivalence branch July 15, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants