Add resetOnChange option to collapseUnchanged #9

Closed
yurks wants to merge 1 commit from yurks/collapse-reset-on-change into main AGit
First-time contributor

Problem

When collapseUnchanged is enabled and the document is replaced wholesale — for example via:

view.dispatch({
  changes: { from: 0, to: view.state.doc.length, insert: value }
})

the merge view shows broken layout: collapsed regions appear in the wrong places, spacers misalign, and the diff becomes unreadable.

This happens because the collapsed-unchanged state field only maps existing collapse decorations through document changes. It never rebuilds them from the current diff. After a full document replacement, the old collapse ranges are mapped across a massive change and end up stale and misplaced.

A workaround exists — calling mergeView.reconfigure({ collapseUnchanged: { margin: 3, minSize: 6 } }) alongside the content update — but that forces consumers to know about an internal implementation detail.

Why this matters

This pattern is extremely common when using @codemirror/merge with reactive frameworks like React or Vue:

  • Props/state drive editor content (doc={sideA} / :doc="sideA")
  • When the parent re-renders with new data, the binding typically replaces the entire document in one dispatch
  • With collapseUnchanged enabled, every such update produces a broken view unless the consumer also calls reconfigure

Any integration that treats the merge view as a controlled component and pushes new document values from outside will hit this.

Solution

Add an opt-in resetOnChange flag to the existing collapseUnchanged config:

new MergeView({
  a: { doc: sideA, extensions: createExtensions() },
  b: { doc: sideB, extensions: createExtensions() },
  collapseUnchanged: { margin: 3, minSize: 6, resetOnChange: true }
})

When resetOnChange: true, the collapsed ranges are rebuilt from scratch whenever the diff changes (detected via ChunkField update, which fires on both editors). When false (the default), behavior is unchanged — collapsed sections and manual expansions are preserved across incremental edits.

Implementation details:

  • The collapse StateField is now created inside collapseUnchanged() so it can close over margin, minSize, and resetOnChange. This replaces the previous global field + .init() pattern and gives each merge view its own field instance (no cross-view interference).
  • The option is documented on both MergeConfig (split view) and UnifiedMergeConfig (unified view).
  • No change to default behavior — existing consumers are unaffected.

Trade-off

With resetOnChange: true, manually expanded collapsed sections are re-collapsed on the next content change. This is intentional and documented: the flag is meant for controlled/reactive use cases where content is replaced externally, not for interactive editing where the user expands sections and then types.

## Problem When `collapseUnchanged` is enabled and the document is replaced wholesale — for example via: ```js view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }) ``` the merge view shows broken layout: collapsed regions appear in the wrong places, spacers misalign, and the diff becomes unreadable. This happens because the collapsed-unchanged state field only **maps** existing collapse decorations through document changes. It never rebuilds them from the current diff. After a full document replacement, the old collapse ranges are mapped across a massive change and end up stale and misplaced. A workaround exists — calling `mergeView.reconfigure({ collapseUnchanged: { margin: 3, minSize: 6 } })` alongside the content update — but that forces consumers to know about an internal implementation detail. ## Why this matters This pattern is extremely common when using `@codemirror/merge` with reactive frameworks like **React** or **Vue**: - Props/state drive editor content (`doc={sideA}` / `:doc="sideA"`) - When the parent re-renders with new data, the binding typically replaces the entire document in one dispatch - With `collapseUnchanged` enabled, every such update produces a broken view unless the consumer also calls `reconfigure` Any integration that treats the merge view as a controlled component and pushes new document values from outside will hit this. ## Solution Add an opt-in `resetOnChange` flag to the existing `collapseUnchanged` config: ```js new MergeView({ a: { doc: sideA, extensions: createExtensions() }, b: { doc: sideB, extensions: createExtensions() }, collapseUnchanged: { margin: 3, minSize: 6, resetOnChange: true } }) ``` When `resetOnChange: true`, the collapsed ranges are **rebuilt from scratch** whenever the diff changes (detected via `ChunkField` update, which fires on both editors). When `false` (the default), behavior is unchanged — collapsed sections and manual expansions are preserved across incremental edits. Implementation details: - The collapse `StateField` is now created inside `collapseUnchanged()` so it can close over `margin`, `minSize`, and `resetOnChange`. This replaces the previous global field + `.init()` pattern and gives each merge view its own field instance (no cross-view interference). - The option is documented on both `MergeConfig` (split view) and `UnifiedMergeConfig` (unified view). - No change to default behavior — existing consumers are unaffected. ## Trade-off With `resetOnChange: true`, manually expanded collapsed sections are re-collapsed on the next content change. This is intentional and documented: the flag is meant for controlled/reactive use cases where content is replaced externally, not for interactive editing where the user expands sections and then types.
When the document is replaced wholesale (e.g. setting a new value in a
reactive framework), the collapsed unchanged ranges were only mapped
through the change, leaving them stale and misplaced. Add an opt-in
`resetOnChange` flag that rebuilds the collapsed ranges from scratch
whenever the diff changes, on both editors.

Co-authored-by: Cursor <cursoragent@cursor.com>
yurks changed title from Add resetOnChange option to collapseUnchanged to Add resetOnChange option to collapseUnchanged 2026-05-30 15:14:24 +02:00
Owner

Please don't send me AI slop. Both your diagnosis and your patch are absolutely useless. Dumping that on me is just rude.

But I guess there's a bug report somewhere hidden in there. Attached patch should make the library handle this better.

**Please** don't send me AI slop. Both your diagnosis and your patch are absolutely useless. Dumping that on me is just rude. But I guess there's a bug report somewhere hidden in there. Attached patch should make the library handle this better.
marijn closed this pull request 2026-05-30 20:42:22 +02:00
Author
First-time contributor

ai slop, useless, rude.. are you in the god mode, dude? In case it's all useless for you, isn't it time to retire?

On subj, isn't your fix a breaking change for in-place diff editing cases? That was only the reason for resetOnChange option instead of just fixing that.

ai slop, useless, rude.. are you in the god mode, dude? In case it's all useless for you, isn't it time to retire? On subj, isn't your fix a breaking change for in-place diff editing cases? That was only the reason for resetOnChange option instead of just fixing that.

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
codemirror/merge!9
No description provided.