fix(core): resolve table widget offsets and source at interaction time#128
Open
changdapeng wants to merge 1 commit into
Open
fix(core): resolve table widget offsets and source at interaction time#128changdapeng wants to merge 1 commit into
changdapeng wants to merge 1 commit into
Conversation
eq() deliberately compares only `source` so CM6 reuses the table DOM when the table's own text is unchanged, and locked dispatches (in-cell typing under the focus lock, range clears under the range lock) keep the widget instance alive per the StateField's decos.map path. But the reused DOM's event closures reference the widget instance that created them, and its captured `tableFrom` and `source` go stale: `tableFrom` as soon as content before the table changes length, `source` as soon as a locked dispatch rewrites the table itself. Every mutation path then dispatches against stale values — when the document got shorter the dispatch throws a swallowed RangeError and the edit silently does nothing; when it grew, the change lands inside unrelated text and corrupts it; a structural edit right after in-cell typing rebuilds the table from a pre-typing snapshot and reverts what was just typed. Keep the DOM-reuse optimization and the eq() snapshot semantics intact and instead resolve both values live at the moment of interaction: `currentTableFrom` via view.posAtDOM (falling back to the captured offset), and `currentTableSource` by re-slicing the block's line span out of the current document (every widget-retaining edit preserves the table's line count). The per-frame selection poll memoizes the lookups per document identity so idle frames stay pure arithmetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoRNuUB2DWz7csbpNvABnz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / 摘要
When document content before a table changes, or the table itself is rewritten by a dispatch made under an editing lock (in-cell typing, range clears), the reused widget's event closures keep mutating the document against their construction-time
tableFromandsource— silently doing nothing when the document got shorter (swallowedRangeError), corrupting unrelated text when it got longer, and reverting freshly typed cell content when a structural edit follows in-cell typing. Mutation paths now resolve both the live position (view.posAtDOM) and the live table source (re-slicing the block's line span from the current document) at the moment of interaction.Motivation / 背景与动机
EditableTableWidget.eq()deliberately compares onlysource, so CM6 reuses the widget DOM whenever the table's own text is unchanged — a sensible optimization (rebuilding table DOM on every keystroke above it would be expensive). But the reused DOM's event listeners close over the widget instance that created them, and that instance'stableFromgoes stale as soon as anything before the table changes length (typing above it, editing a previous table). Every mutation path then dispatches against the stale offset:RangeError: Invalid change range …is thrown inside the event listener and swallowed; the user's action silently does nothing;Reproductions (both verified against main in a real browser):
RangeError, drag silently ignored.Intro+ table → extend the intro text → drag a column ⇒ document becomesIntro —| b | c | a | … --- | --- |— the intro is partially eaten, old table fragments remain.Changes / 变更内容
packages/core:src/live-preview-table.ts— the widget keeps a reference to its rendered wrapper; new privatecurrentTableFrom(view)resolves the live start offset withview.posAtDOM(guarded bycontentDOM.contains, wrapped in try/catch mirroring the existinggetPosAtDOMidiom ineditor.ts, falling back to the capturedtableFrom), andcurrentTableSource()re-slices the live block by its line span (every widget-retaining edit preserves the table's line count). All mutation paths —dispatch(), the structural edits,addRow(), the cell-edit save — use them; thecheckSelectionrAF loop memoizes the lookups per document identity so idle frames stay pure arithmetic.eq()and the DOM-reuse optimization are intentionally untouched: the render-timesourcesnapshot is exactly what eq() diffs against the next rebuild to refresh the DOM, so it must NOT be overwritten (documented in Table Widget rule 13).CLAUDE.md— added Table Widget rule 13 encoding this invariant, following the file's convention of recording landmines discovered inlive-preview-table.ts.packages/core/test/live-preview-table-offsets.test.ts— 4 cases driving the real event chains (no layout/coordinate dependency): cross-table sequential edits (shrinking document), text-above-grows-then-edit (asserts the surrounding prose survives byte-for-byte), type-into-cell-then-delete-column (asserts the typed text survives a structural edit made while the cell edit is still hot), and a no-prior-edit baseline guarding first-interaction behavior.Testing / 测试
pnpm testpasses — 519/519 (515 on main + 4 new)pnpm buildfor @floatboat/nexus-core)pnpm typecheck— zero errors;git diff --checkcleanCompliance / 合规自检
AI-assisted notes / AI 使用说明: The functional code in this PR was primarily developed by AI (Claude Code) under my direction; I reviewed it and take responsibility for every line.
Checklist / 自检清单
live-preview-table.ts→ walked through the 12 Table Widget rules:eq(), editing locks, rAF clear conditions, drag lifecycle andonDragEndordering all untouched; the diff adds offset resolution onlyRelationship to the ragged-tables PR
Logically independent fix, branched directly off main. Textually the two PRs edit the same structural-edit method bodies, so whichever lands second needs a mechanical rebase: the union is passing this PR's live source as the other PR's transform input, e.g.
this.dispatch(deleteTableColumn(this.currentTableSource(), colIdx)).