Skip to content

Host the JSON dictionary editor on view's JSON dictionary engine (6b V3) - #171

Merged
ww-mw merged 3 commits into
mainfrom
adopt/json-engine
Oct 31, 2026
Merged

ww-mw merged 3 commits into
mainfrom
adopt/json-engine

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Step 6b, V3: the JSON dictionary editor becomes a host over view's JSON dictionary engine (JsonEngine, data-explorer-view/host, view v1.2.4), over its TextDocument. The engine was lifted in 6a from this file at c73da3c, which is unchanged since, up to main. It now does:

  • the tables' paints, and the narrow repaints that keep them in step with every change of the text (an edit's echo, an undo or a redo, a keystroke in the text view, a reload);
  • every gesture, and the queue that keeps the structural ones to one text state at a time;
  • the Status column's baseline, the file's stamp, and the clip mark.

This extension keeps VS Code's end of each hook and what is its own: the TextDocument (its edits are WorkspaceEdits; its undo, redo, dirty state, save, revert, reload and hot-exit backup are VS Code's own, as before), the webview, the messages every table answers alike, "Location in Text"'s reveal, and the Add gallery's keybinding.

No behaviour change is intended, and none is named. The P4 runners give the same saves as main's V0c run and as V2's, file by file, with the same answers. JSON's undo/redo sweep, which could not undo locally, ran on CI's window and is equal too (below). The screenshot and posted-message A/B is identical. The integration suites are unchanged and pass.

Commits:

  1. Pin the data-explorer-view v1.2.4 release. Same shape as V1's and V2's pins. v1.2.4 (view Pin core v1.36.7 #58) adds, on ./host:
    • defaultRegistry and the DocumentRegistry type;
    • the JSON engine's trailerAfterDelivery;
    • its opened(view) matched by identity;
    • a table's own changed.
  2. Host the JSON dictionary editor on view's JSON dictionary engine.
  3. Keep a JSON document's engine to its own open TextDocument (review171 L1). The engine a document's tables share reads and writes the TextDocument it was made for, and a later table reused it whatever TextDocument it was handed. A table that was never closed (a resolve that threw before its panel's dispose was wired) would then have handed a later tab an engine over a closed document's text, whose edit ranges would be applied, by uri, to the live document. The old per-tab reads could only have shown a wrong Status. Not reachable in normal use, but new with this PR, so it is fixed here, with two guards:
    • perDocument.ts: a held engine is used only while it fits, i.e. it is the same TextDocument and still open (sameOpenDocument). One that does not fit is disposed first, and the table makes a fresh one. A stale table's later close then disposes nothing.
    • The provider's wiring runs in closingOnThrow, so a resolve that throws after its table opened closes the table before the throw goes on. The normal path's posts and registrations are unchanged; the diff is a re-indent plus five lines.
    • 5 unit tests (perDocument.test.ts); each guard was mutated, and the mutation fails them.

What moves where

  • SlddTextEditorProvider.ts goes from 2,293 lines to 172. It keeps resolveCustomTextEditor: the webview, the table ThemeIcon, the shared messages, the dispatch chain (whose branches now call the engine's table), "Location in Text"'s reveal (showTextDocument beside the table, with its "Failed to locate in text" catch), the Add gallery and the navigate wiring.
  • jsonDocument.ts (new) is the document: the engine, with VS Code's end of its hooks.
    • registry: view's defaultRegistry, where this extension's other readers of a uri's tree find it.
    • text: the TextDocument, through textDocumentStore.ts.
    • registers: hubRegisters().
    • notify: V1's mapping (this engine says only a transfer's warning).
    • usage: annotateDataRows / annotateDataRowsNow.
    • usageReposts: subscribeUsageRepost.
    • disk: fs.stat and fs.readFile of the uri, null on a throw.
    • trailerAfterDelivery, and each table's own changed (the provider's onChanged).
    • One engine per open document (perDocument.ts): made by its first table, disposed with its last, shared across provider instances. That is the old module-level openDocs.
  • textDocumentStore.ts (new, vscode-free) is the TextDocument as the engine's text, call for call:
    • an edit: one WorkspaceEdit holding every patch in order, its ranges taken against the text as it stands, built before anything is awaited; its answer, false and a rejection included, passed through;
    • listening: a subscription of its own per listener, filtered by uri, so listeners keep their order (extension.ts's keystroke invalidate first, then the tables in the order they opened);
    • a change: its replacements as the event lists them, with undo and redo said;
    • reads: text, version and dirty read when asked.
    • It never reloads: VS Code reloads a clean TextDocument from its file itself, and the engine tells a reload by stat, then read, as before.
  • engineTable.ts:
    • jsonRows joins the other two setRows builders, with hasTextView: true;
    • ready opening the table becomes its own flag (readyOpens, false for the binary editor), apart from awaitRows;
    • an open's answer is painted once however many readys asked for it. Two readys before the first paint lands were answered by the newest repaint alone (wideLanding 'superseded').
  • jsonEntryScan.ts, entrySplice.ts, jsonEntrySync.ts and slddFormat.ts were core 1.54.0's code word for word: 0 code lines differ from core's src/edit/ files, comments and imports aside.
    • Each is now a one-statement re-export from data-explorer-core, so its importers are unchanged until the tests are rehomed in V6: 30-odd unit test files, goldens/views.ts, the bench, two integration suites, extension.ts, the binary providers and structuralEdit.ts.
    • entrySplice.ts's findEntryElementSpan is findJsonEntryElementSpan as findEntryElementSpan. Core's own findEntryElementSpan is the binary dictionary's XML finder, so a plain re-export would have handed every JSON caller the wrong function. A unit test pins the alias.
  • Kept whole, and no longer in the bundle, for their tests:
    • structuralEdit, clipboardAction, clipMarks, entryOps and xmlStructuralEdit, until V4. Core's text half takes a sectionRowId builder, and the model half is not core's.
    • slddBaseline, wideRepaint and editTextRequest, until V6. View exports none of their functions, and goldens/views.ts mirrors the paint with slddBaseline.

Order differences, none of them observable

  • The drain and onChanged. A wide paint's drain of a Usage link's selection now comes after onChanged, which is in the engine's trailer. Before, it came just before it (SlddTextEditorProvider.ts:514 and :515 at main). The drain posts only to this table's webview, and onChanged only to the Property Inspector's (refreshInspector → inspector.refresh). Each webview's own sequence is unchanged.
  • The cross-document deleter reads the source text one await sooner. Its openTextDocument(uri) was this same TextDocument, which the engine holds.
  • An open's answer is posted a microtask later than :483 posted it, through the relay's .then, as V1's MAT open is.
  • A V2 difference, in the raw object only. The relay omits beforeRowId from insertEntryRows when it is undefined, where both dictionary editors spelled the key (main's :627-633; the binary editor's old 907-911). VS Code serializes a webview message as JSON, so the page never saw the key. No suite reads it. No change.

Test changes, and why

The vscode integration suites are unchanged: no file under test-integration/ is touched.

Unit tests (vitest) that read this extension's source follow the code there. Each keeps its name unless said:

  • test/messageDispatch.test.ts, "SlddTextEditorProvider.ts answers requestChildren only if its payload defers rows". Its defersRows probe also counts a table of the JSON engine (openJsonTable(), which is planned by JsonView with the same planner as before. The setRows read already followed setRows: jsonRows into engineTable.ts. Still 4 posts, each with docUri.
  • test/multiSelectInvariants.test.ts, "SlddTextEditorProvider pastes and drops through runTransfer". The engine branch it gained in V2 also covers openJsonTable(: paste and drop go to the engine's table, which runs view's runTransfer.
  • test/slddParts.test.ts: "and the file that navigates the part takes the names from core" becomes "and no host file navigates the part: the scan that does is core's". jsonEntryScan.ts is now core's scan, re-exported and nothing else. The test asserts exactly that (its code is one export { … } from 'data-explorer-core', and its scanEntries is core's), and the absence scan beside it is unchanged. This is the change the 6b plan named for V3.
  • test/engineTable.test.ts: the two binary-mode tests pass readyOpens: false beside awaitRows: false, because ready opening the table is now its own flag. Names and assertions are unchanged.
  • vitest.config.ts adds src/host/jsonDocument.ts to the coverage exclude list, because it imports vscode. textDocumentStore.ts and perDocument.ts are vscode-free, so they are measured.

Inventory

  • 2,227 tests (main, cc8cb1b) become 2,250, by name: 3 renamed, 23 new, none removed or moved. The inventory compare (main against this branch) reports 0 missing apart from the 3 renames, and 0 no longer passing.
  • Renamed: slddParts' case (above); viewPin's two cases now name v1.2.4.
  • New: textDocumentStore.test.ts (7), perDocument.test.ts (3), coreReExports.test.ts (5: each re-export is core's own, and entrySplice's alias is the JSON finder, which finds a JSON entry where core's XML one finds nothing), engineTable.test.ts's JSON mode (3: the paint order with the drain last and the jsonRows keys; two readys, one paint; rows that cannot be delivered), and commit 3's 5 in perDocument.test.ts (an engine of another TextDocument, or of a closed one, gives way to a fresh one; a throwing resolve leaves nothing held; a resolve that does not throw keeps its table; the provider wraps its wiring). The one-paint guard and each L1 guard were mutation-checked.

Gate

  • npm run verify (on df210e4): typecheck (bench included), both builds, 141 files and 2,250 tests, and check:leak.
  • npm run test:integration (with the runners' environment set): 0 failing in either run.
    • With the MAT and gesture runners set: 457 passing, 7 pending.
    • With editorSaveSweep set: 456 passing, 8 pending.
    • Each run's pending tests are the runners not set in it, the DEX_GATE_FIXTURES live gate, and 4 self-skips that V2's runs did not have: jsonTableUndoRedo's three round trips and cutPasteUndo's undo assertion ("SKIPPING: this VS Code host does not apply undo …").
    • Cause of the skips: the Mac's screen was locked from 17:27 (the user away). A locked session gives no window keyboard focus, so undo never reaches the test window. Both runs skipped the same 4, run 2 with no other window open. These 4 contracts run on CI's xvfb window, which never locks. On this PR's CI (run 41141193329) all four passed:
      • jsonTableUndoRedo, attempts 1 and 2: "✔ cell edits: undo saves the original bytes, redo saves the edit's", "✔ adds and deletes: …", "✔ the gallery, a paste and a move: …";
      • cutPasteUndo, attempt 2 (job 124358110099): "✔ a same-document move applies as ONE WorkspaceEdit and a single undo restores the original (137ms)".
      • Attempt 1 failed that one cutPasteUndo case (457 passing, 10 pending, 1 failing): "timed out after 20000ms waiting for one undo to restore the clean original". It is the known flake: the same test, with the same 20 s undo timeout, failed main's CI on e73f001 and 123e35e and passed on a re-run. It runs none of this PR's code: it applies one full-range editor.edit itself and undoes with default:undo, with no table and no provider. The failed job was re-run once, and attempt 2 passed (458 passing, 10 pending).
    • Totals: both runs add up to 464 tests. CI runs the merge ref with Add a runner that writes the live gate's files, a folder per writer #170, so it shows 4 more pending: liveGateSaves.test.ts's 4 tests, skipped unless DEX_GATE_OUT is set. It also shows one more unit test (integrationSleeps' check for that file).
  • Goldens 159 of 159; the full export (469 files) is identical to main's. goldens/views.ts keeps reading the kept copies and the re-exports.
  • Screenshot + posted-message A/B, main against this branch (d2bf948): commit 3 is host-only, and its webview bundles are byte-identical to main's again, so the A/B, which renders those bundles over the goldens' payloads, stands for it. 5,712 of 5,712 screenshots identical (12 skipped, as on main); all 5,724 scenarios' posted messages identical; shells identical, and the webview bundles byte-identical.
  • vsce ls: the same 136 files as main.
  • Bundles: dist/webview is byte-identical. The host bundle shrinks by 2,133 B (desktop) and 2,118 B (web), because the duplicates drop out:
    • view's jsonView.js +21,893 B and jsonEngine.js +1,053 B, against the provider's −13,541 B;
    • this extension's copies that leave the bundle: structuralEdit −6,146, entryOps −3,990, the parts of transfer.ts only the JSON editor used −2,526, clipboardAction −845, slddBaseline −734 (view's +210), clipMarks −410, refusal −299, editorHub −242, editTextRequest −162, wideRepaint −116 (view's +116);
    • the four re-exports swap this extension's copy for core's: jsonEntryScan (2,104 B), jsonEntrySync (1,920 B) and entrySplice (1,308 B) at the same size, and slddFormat at 339 B against core's 319 B;
    • core's structuralEdit +1,314; the new jsonDocument.ts +1,310, textDocumentStore.ts +746, perDocument.ts +313, engineTable.ts +204.

P4, through the real editors (#166's runners), on this branch

On this branch's build (the switch on the 1.2.4 tarball, fdbaaa1, whose dist/ is byte-identical to the v1.2.4 tag's, which this branch pins). The output is in /tmp/eb-run-v3 (MAT and gestures) and /tmp/eb-run-v3b (plain edits).

set vs main's V0c run (/tmp/eb-run-v0c-main) and V2's (/tmp/eb-run-v2*) vs the 6a references
MAT edits 2,455 of 2,455 equal: answers, and saves byte for byte 2,455 byte-equal to /tmp/eb-run-p4mat
MAT gestures 201 of 201 equal: 172 written (bytes and added names), 29 refused in the same words gesture-core: the same
sldd gestures (365 JSON + 358 binary) 723 of 723: every answer, reason, save and cross-document source equal; see "the 700" below 660 of 660 EQUAL vs the replay (p4sldd); 723 of 723 EQUAL vs real VS Code (p4real)
binary undo / redo 345 + 345 complete and equal (zip members, LastMod masked) as main
JSON undo / redo on CI's window (below): 350 undo saves equal the original, 350 redo saves equal the gesture's save; every save EQUAL vs v0c-main and V2 365 of 365 EQUAL vs real VS Code (p4real)
plain JSON edits 2,434 of 2,434 EQUAL, 63 whole files byte-equal 2,434 of 2,434 EQUAL vs /tmp/eb-run-p4c1521
plain binary edits 2,469 of 2,469 EQUAL (zip members in order, LastMod masked) 2,469 of 2,469 EQUAL vs /tmp/eb-run-p4c1521

"The 700". Against v0c-main and V2, compare-gesture reports "a saved file differs: 700". All 700 are "undo only in the reference" or "redo only in the reference": the JSON undo and redo saves, 350 + 350, which those runs made with the screen on and this run could not ("not run: this host's undo did not reach an editor"). Counted from the case logs, the 700 are exactly those, and no saved, source or binary undo/redo file differs. So there is no differing file to diff by hand. Against p4sldd and p4real the same cases compare EQUAL, because those references never ran the JSON undo/redo either.

The V2 compare's traceback ("sldd gestures vs V2"): compare-gesture.mjs reads the case list from the reference's folder (select-sldd-gesture.jsonl), which V2's output folder did not hold. With that list copied in (the same file both runs used, identical to v0c-main's), it gives the same 723 cases and the same 700 JSON undo/redo saves only in V2.

Re-gate on commit 3 (df210e4), through the real editor, alone (no other VS Code window). The output is in /tmp/eb-run-v3l1.

  • Integration: 457 passing, 7 pending, 0 failing. The display was asleep, so it has the same 2 SKIPPING lines (jsonTableUndoRedo, cutPasteUndo) as above.
  • CI on df210e4 (run 41145951106): green on the first attempt, 458 passing, 10 pending. Quoted: "✔ cell edits: undo saves the original bytes, redo saves the edit's (1904ms)", "✔ adds and deletes: … (1062ms)", "✔ the gallery, a paste and a move: … (949ms)", "✔ a same-document move applies as ONE WorkspaceEdit and a single undo restores the original (141ms)".
  • Plain JSON edits: 2,434 of 2,434 EQUAL vs v0c-main and vs p4c1521, 63 whole files byte-equal.
  • JSON gestures: 365 cases, 0 answer or reason differences. Against v0c-main and against the CI run above, the only differences are the 700 JSON undo/redo saves that those runs hold and this one could not make, with the display asleep. Counted by name: 350 undo and 350 redo only in the reference, and no save or source differs.

JSON undo/redo 350 + 350, on CI's window.

  • Why it did not run locally: this run's 0 of 350 was mostly the gesture runner caching its first miss. Case 1's undo missed while the display was asleep, and the runner then skipped the other 349. V3 applies JSON edits and undoes them by the same path as main (workspace.applyEdit, then VS Code's undo). Let the gesture sweep move across formats and drag #173 fixes the runner (every case tried, one retry).
  • How it ran: draft CI run of the JSON undo/redo sweep for #171 (not for merge) #172, closed unmerged, was this branch plus one commit. That commit added the JSON half of the case list (365 cases), placed the originals at the paths the list names (core's fixtures at v1.52.0 and test/fixtures/arch.sldd), set the sweep's environment, and uploaded the saves. Its CI run 41141477830 (Linux, xvfb) is the evidence, in the artifact json-gesture-sweep.
  • Complete: 350 cases written, each with an undo save and a redo save; 15 refused, as before; no "not run".
  • Equal:
    • undo = the original byte for byte, 350 of 350; redo = the gesture's own save, 350 of 350;
    • every save, undo and redo EQUAL vs main's V0c run (macOS) and V2's, with each entry's lastmod and new UUIDs masked, as locally;
    • the 365 cases EQUAL vs this branch's local run and vs real VS Code (p4real).
    • So no CI "before" from main was needed.

Risks, data loss first, and what holds each

  • R1, the write. One WorkspaceEdit per gesture holds every patch, built synchronously against the text as it stands, and its answer is passed through. A false or a rejection reaches undoPaint or the repair as before. A rename's catalog ranges stay in the same edit, so the rename is still one undo step. Held by jsonRebuildDuringWrite, functionRenameThroughEditors, textDocumentStore.test.ts and P4.
  • R2, edit, paint, undoPaint and the echo rules. A gesture paints, then writes. The echo is proven byte for byte against hostEdit/paintedWrite (one-shot), and an undo or redo is recognised from knownEdits. All of it needs every change delivered synchronously, in VS Code's order, with its replacements untouched, one listener per table: that is the adapter's whole job. Held by undoOfTableEdit, undoRepaintsFromModel, jsonDirtyEventRepaint, jsonRenameRepaint, cutPasteUndo, jsonHostEditRepaint and jsonEntryScopedSync (unit), and P4 undo/redo.
  • R3, dirty and the baseline. dirty is read live off the TextDocument while a change is heard, so it gives the state before the change; the empty change after it carries the new one. Held by jsonBaselineFollowsDisk, twoTabBaseline and dirtyLifecycle (unit).
  • R4, disk follow. The reload is VS Code's own; there is no reset. A reload is told by stat, then read. Held by jsonBaselineFollowsDisk.
  • R5, hot exit. The backup of the text is VS Code's. A restored document opens dirty and is baselined from the file. Held by the dirty-open cases.
  • R6, queueStructural. The queue is inside the engine, one text state at a time; the dispatch no longer queues. Held by multiDelete, multiPasteSelection, heldPaste and crossDocumentMove.
  • R7, the text-sync limit. Routing is unchanged (extension.ts:70, BinaryEditorProvider's misroute rule), now through core's predicates by the re-export: the same code and the same 50 MiB.
  • R8, the registers and the deleter. hubRegisters, as in V1. The cross-document deleter edits through the engine's text. Held by crossDocumentMove, mptPasteDrop, refusedPasteDrop and P4's cross-document moves.
  • R9, the registry. The engine holds its trees in defaultRegistry, where extension.ts's keystroke invalidate, the Property Inspector, the matrix answer and the read-only view's count find them. Held by view Pin core v1.36.7 #58's registry test, closedTabRelease and jsonInspectorRefresh.
  • R10, message shape and timing. jsonRows, the trailer after delivery, one paint for two readys, and the order notes above. Held by engineTable.test.ts, the suites' raw posts and the A/B.

For later

  • V4 replaces hubRegisters with view's createRegisters() and deletes the shared copies kept here: structuralEdit, clipboardAction, clipMarks, entryOps, transfer and xmlStructuralEdit.
  • V6 rehomes the engine-logic tests (D48), then deletes the re-exports, slddBaseline, wideRepaint and editTextRequest.

ww-mw added 2 commits October 30, 2026 18:45
v1.2.4 adds what the JSON dictionary editor needs to move onto view's
JSON dictionary engine, on ./host only: defaultRegistry (the registry
over core's module-level session that getModel, peekModel, findNode and
invalidate answer from) and the DocumentRegistry type, the engine's
trailerAfterDelivery, opened(view) matched by identity, and a table's
own changed. Nothing changes for apps.

The lockfile resolves the tag to its commit, 1059c6f (the ^{} line of
ls-remote), at version 1.2.4, with no integrity field; view's core peer
is still ^1.54.0. viewPin.test.ts moves with it.
SlddTextEditorProvider is now a host over data-explorer-view's
JsonEngine, over its TextDocument: the tables' paints, the narrow
repaints that keep them in step with every change of the text, every
gesture and its queue, the Status column's baseline and the clip mark
are the engine's, lifted from this file. What stays is VS Code's end of
each hook and what is this extension's own: the TextDocument (its edits
are WorkspaceEdits; its undo, redo, dirty state, save, revert, reload
and hot-exit backup are VS Code's, as before), the webview, the messages
every table answers alike, Location in Text's reveal, and the Add
gallery's keybinding.

- jsonDocument.ts: the document, the engine with VS Code's end of its
  hooks: view's defaultRegistry (where this extension's other readers of
  a uri's tree find it), the TextDocument as its text, the registers,
  notices, the Usage column and its reposts, the file's stamp and bytes,
  trailerAfterDelivery, and each table's own changed (the provider's
  onChanged). One engine per open document, made by its first table and
  disposed with its last (perDocument.ts), as the module-level document
  state was.
- textDocumentStore.ts: the TextDocument as the engine's text, call for
  call. One WorkspaceEdit per edit with every patch in order, its ranges
  taken against the text as it stands, its answer passed through; a
  subscription of its own per listener, so the listeners keep their
  order; text, version and dirty read when asked. No reload of its own:
  VS Code reloads a clean TextDocument itself.
- engineTable.ts: jsonRows, with hasTextView; `ready` opening the table
  is its own flag (readyOpens), apart from awaitRows; and an open's
  answer is painted once however many readys asked for it, as the
  newest repaint was the one that landed.
- jsonEntryScan.ts, entrySplice.ts, jsonEntrySync.ts and slddFormat.ts
  were core 1.54.0's code word for word, and are now re-exports of it,
  so their importers are unchanged. entrySplice's findEntryElementSpan
  is core's findJsonEntryElementSpan: core's own findEntryElementSpan is
  the binary dictionary's XML finder.

No behaviour changes. Tests that follow the code: messageDispatch's
defersRows probe and multiSelectInvariants' engine branch also count
openJsonTable; slddParts' "and the file that navigates the part takes
the names from core" becomes "and no host file navigates the part: the
scan that does is core's"; the binary relay tests pass readyOpens:
false; vitest.config excludes jsonDocument.ts from coverage. New:
textDocumentStore, perDocument, coreReExports and engineTable's JSON
mode.
Review L1. The engine a document's tables share reads and writes the
TextDocument it was made for, and a later table reused it whatever
TextDocument it was handed. A table that was never closed (a resolve
that threw before its panel's dispose was wired) would then hand a
later tab an engine over a closed document's text, whose edit ranges
would be applied, by uri, to the live document: a wrong write, where
the editor's own per-tab reads could only have shown a wrong Status.

- perDocument.ts: a held object joins a table only if it `fits`; one
  that does not is disposed first (with the tables still holding it,
  whose close then has nothing left to dispose), and the table makes a
  fresh one. sameOpenDocument: the same TextDocument, still open.
- jsonDocument.ts holds each engine with its TextDocument and opens a
  table only on an engine of that document, open.
- SlddTextEditorProvider.ts: the tab's wiring runs in closingOnThrow, so
  a resolve that throws after its table opened closes it before the
  throw goes on. The normal path's posts and registrations are
  unchanged.

Tests (perDocument.test.ts, 5 new): an engine of another TextDocument,
and of one closed since, gives way to a fresh one, which a stale
table's close leaves alone; a throwing resolve leaves nothing held; a
resolve that does not throw keeps its table; and the provider opens its
table in closingOnThrow, with the dispose wired inside it. Each guard
was mutated, and the mutation fails these tests.
@ww-mw
ww-mw merged commit 9f30189 into main Oct 31, 2026
4 checks passed
@ww-mw
ww-mw deleted the adopt/json-engine branch October 31, 2026 00:04
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.

1 participant