Repository navigation
refactor(cm): build table and image block widgets from one structural block contract - #487
Merged
Merged
Conversation
… (red)
Revert-check evidence for ARCH-07 PR3. Committed red on purpose against
the pre-contract table and image block fields; the rows go green at the
end of the Step 7 migration.
`pnpm vitest run test/webview/cm-structural-blocks.test.ts -t "failure table|failure image|widget identity table"`
on this commit (mount preconditions pass; each row fails on its
behaviour assertion):
- failure table: AssertionError: expected [Function] to not throw an
error but 'Error: parseTable armed to throw' was thrown
- failure image: AssertionError: expected [Function] to not throw an
error but 'Error: commonMarkAltText armed to throw' was thrown
- widget identity table: AssertionError: expected TableBlockWidget{ …(5) }
to be TableBlockWidget{ …(5) } // Object.is equality
…ts (red) Row "frontier advance": a doc whose init snapshot is partial, the parse context advanced to the end by ensureSyntaxTree without a republish, then a single prose keystroke far above a table and an image. Every precondition passes on this commit, including the two that put HEAD on its bounded path (touchesStructuralReparse(tr) === false, syntaxTreeAvailable(tr.state, doc.length) === true), and the behaviour assertion fails: AssertionError: expected [] to deeply equal [ 20070 ] (the image widget beyond the old snapshot is never published) Verdict: the gap is real, so the frontier-advance arm stays in scope. The table half passes here only because this row mounts tableBlockField without tableSkeletonField, and that fallback full-walks on every change; the production skeleton field takes the bounded path like the image one.
…ainment, ownership)
structuralBlockWidgets(spec) builds one StateField whose value is
{ records, decorations }. This commit lands Steps 4a-4c of the ARCH-07 PR3
plan: the full walk, recordAt, hook containment (build in the walk, widget in
makeRecord, one latched console.error per arm that never reads err), and range
ownership (frontmatter filter, from>=to / containment drop reported as
errKind "invalid-range", mapped fmEnd comparison with the 0 sentinel).
Toy rows: "toy full", "toy build throws", "toy widget throws",
"toy ownership". Revert-checked: replacing either catch with a rethrow turns
"toy build throws" / "toy widget throws" red ("Error: toy build" /
"Error: toy widget").
Steps 4d-4i of the ARCH-07 PR3 plan. The bounded update, rebase through makeRecord, the visited dedupe and the tree-identity arm landed with the module in the previous commit; this commit adds their toy rows: "toy bounded update", "toy rebase", "toy widget throws on rebase", "toy dedupe", "toy tree identity", "toy branching", "toy seeded oracle". Revert-checked: dropping the visited check turns "toy dedupe" red, and returning prev on a republished tree turns "toy tree identity" red.
Step 4j of the ARCH-07 PR3 plan: row "reveal" (toy spec) — a caret on a block line hides its decoration while the record stays; leaving restores the same widget object; a same-line-span caret move returns the field value.
Rows "frontier pin table" and "frontier pin image" run the production parser with stopAt(p) for every p strictly inside the complete target, fresh and incremental (TreeFragment.addTree + applyChanges, for a distant prose edit and an edit inside the target). The partial tree must hold the target with its complete range or not at all. This is the evidence that the structural block contract needs no "settled" rule; it goes red if a future @lezer/markdown cuts a leaf block at stoppedAt.
…apshot frontierIncomplete(tr) is the frontier half of requiresFullBoundedRebuild. Besides G2 (post-edit context incomplete) it is true when the records were built over a partial SNAPSHOT: after ensureSyntaxTree the parse context is complete while syntaxTree(startState) is still partial, so the first docChanged transaction took the bounded path and never published blocks past the old snapshot end (Step 2 reproduced this for table and image). The docblock is the canonical explanation of the snapshot/context split. fenced-code-collapse docChangePlan calls frontierIncomplete in place of its own syntaxTreeAvailable term and keeps its narrower structural check. The frontier-gate guard counts frontierIncomplete calls by name (ALLOW: structural-guard 2, fenced-code 1) with a non-vacuity fixture. Rows frontier advance fenced/fold/callout pin the other consumers. Revert checks: restoring the old fenced term reds only the fenced row; restoring the old guard term reds the base, fold and callout rows.
…block contract
The table and image block fields are now specs of structuralBlockWidgets
(structural-blocks.ts): each says only which Lezer node it looks at, how to
build its block and which widget shows it. Range ownership, the bounded
update, the full-rebuild reasons, failure -> raw source and reveal live in the
contract. The field value is now { records, decorations }.
- table: tableBlockField replaces tableSkeletonField + the old table field;
table-skeleton.ts, the resolveModels fallback and its warn latch are gone.
table-ranges.ts stays for the lint rule only.
- image: image-field.ts keeps its eligibility rules, render gate and alt
extraction; computeBounded / computeExtendedSpan / G3 are gone.
- markdown.ts tableEmitsBlockWidget reads the records through recordAt.
- Tests read .decorations / .records; cm-table-skeleton.test.ts ports to
cm-table-blocks.test.ts; cm-table-fallback-warn.test.ts is deleted; the
fold blockquote harness registers tableBlockField explicitly, plus a row
without it.
- New production rows: ownership table/image, frontmatter body
insert/delete, no frontmatter offset-0 insert, viewport outside
table/image, reveal table; "structural build failure keeps sync" in
cm-widget-containment.
Revert-checks run before committing:
(i) frontmatterMoved -> unmapped `oldEnd !== newEnd`:
x frontmatter body insert: one character inside the body rebases the blocks
AssertionError: expected [ ...(2) ] to deeply equal []
x frontmatter body delete: one character out of the body rebases the blocks
AssertionError: expected [ ...(2) ] to deeply equal []
no frontmatter offset-0 insert: green
(ii) frontmatterMoved -> `tr.changes.mapPos(oldEnd, 1) !== newEnd` (no sentinel):
x no frontmatter offset-0 insert: a keystroke at offset 0 rebases the blocks
AssertionError: expected [ ...(2) ] to deeply equal []
(iii) build catch in structural-blocks.ts -> rethrow:
x structural build failure keeps sync: a throwing parseTable never stops edit-sync
AssertionError: expected [Function] to not throw an error but
'Error: parseTable armed to throw' was thrown
All restored; structural-blocks.ts unchanged by this commit.
… import frontierIncomplete The full suite went red on two guards the earlier steps did not run: - no-bare-unstarved-gate flagged the two bare expect(syntaxTreeAvailable(...)).toBe(true) gates in the frontier-advance rows. They now go through withUnstarvedFrontierState, the repo's load-robust form. Non-vacuity rechecked: with the snapshot term of frontierIncomplete forced to false, all four frontier-advance rows go red. - cm-lezer-table-internals-tripwire pinned that fenced-code-collapse.ts never imports structural-guard.js, which the frontierIncomplete step made false by design. It now pins that the one import names frontierIncomplete alone (red when requiresFullBoundedRebuild is added to it).
The pre-9R shape kept a parallel records array next to the DecorationSet
and allocated fresh block/record objects for every shifted block on each
edit. Measured cost: ~1.8-2.1 MB retained over HEAD; image-1200 prose@mid
retained 8580 KB on the branch vs 6439 KB on HEAD, failing the Step 10
heap bar.
Now the DecorationSet is the store: the position-free payload rides
deco.spec.structural, StructuralRecordSet<B> brands it at compile time,
recordAt scans records.between, a moved record reuses its payload, an
all-unchanged update returns the previous set, and reveal shares the
record set as the decoration set when no selection meets a block.
Revert-checks (9R-d):
- payload spread on rebase -> "toy rebase":
AssertionError: expected { text: '```\nalpha\n```' } to be { text: '```\nalpha\n```' } // Object.is equality
- meetsSelection short-circuit removed -> "reveal":
AssertionError: expected RangeSet{ chunkPos: [ 18 ], …(3) } to be RangeSet{ chunkPos: [ 18 ], …(3) } // Object.is equality
…uctural-range-model
Return early from tableEmitsBlockWidget when the table field is absent, and rewrap comment lines past 100 columns. No behaviour change.
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
Table and image block widgets now come from one small structural-block contract. The contract covers tree identity, the settled range, the changed range and ownership. A range that is unparsed, over budget or throwing falls back to raw source, and input and edit-sync keep working while it is raw. Block widgets stay StateFields, and the decoration set itself is the record store.
Changes
src/webview/cm/structural-blocks.ts. It holds the types, the walk, the containment and ownership checks, and the bounded update arms. Ownership and determinacy derive from the contract.frontierIncomplete) stays, which is the Step 2 verdict.DecorationSetis now the record store, and the payload is position-free.Related
Performance gate (plan revision 14)
Test Plan
pnpm compileenv -u ELECTRON_RUN_AS_NODE pnpm test:e2epnpm packageand a forced install (0.1.86)