Skip to content

Keep the JSON dictionary table in step with its text after re-reads, renames, reloads and closes - #110

Merged
ww-mw merged 15 commits into
mainfrom
json-sync-fixes
Oct 24, 2026
Merged

ww-mw merged 15 commits into
mainfrom
json-sync-fixes

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 24, 2026

Copy link
Copy Markdown
Member

Fixes from a review of the JSON text dictionary editor's text-to-table sync, plus a host catalog fix. The table and the plain-text view show the same document; the provider repaints narrowly when it can prove that is right and re-reads otherwise. These close the gaps where it trusted a narrow path it should not have.

  • A table edit's echo is no longer swallowed after a full re-read. A save, a webview reload or a clipboard repaint could re-read the pre-write text between a host edit and its echo; the echo was then dropped and the table kept a row the text no longer had. Echoes now carry the epoch they were painted in.
  • A full repaint that lands late cannot overwrite a newer narrow update. Overlapping repaints land in order, and two views of one document cannot ping-pong.
  • A rename's echo uses the renamed row's id, so a rename no longer ends in a full repaint (about 1.4 s on a 47.8 MB dictionary).
  • Modified marks follow the file. They re-baseline when the document is clean after a reload, revert or undo back to saved. A document reopened dirty (hot exit) baselines from the disk bytes. Checking the disk stats the file first and reads it only when the stamp changed (48.7 MB: read 14–45 ms, stat 0.5–4 ms).
  • A closed view no longer re-registers a dictionary tree or posts to its webview. It also posts nothing before ready.
  • The Properties panel stays current with a JSON dictionary edited from the table or the text, through the shared refresh the binary and MAT-file editors use. It is deduped, so unrelated keystrokes post nothing.
  • System Composer catalog rename is scoped to Design data, as core now scopes it. Renaming an Other Data or Configurations entry that shares an interface's name no longer renames the interface. A test holds host and core to one rule.

Tests: unit 4322 passed; integration 420 passing / 2 pending, 0 failing, on this branch merged with main.

ww-mw added 15 commits October 23, 2026 21:00
…its write

A delete, paste, drop or add paints before it writes, and the change event its write fires was
skipped as an echo of rows already on screen. A save, a webview asking for its rows, or a
clipboard broadcast over rows out of step repaints the whole table without waiting for that write,
and reads the text from before it: the screen went back to the old rows, the tree was rebuilt from
the old text, and the skipped echo was the one change that would have repaired both. The table and
the tree then said an entry existed that the file no longer had.

Each view now counts the wide repaints it has begun, and a painted write and a cell edit's hint
carry the count from when they were painted. An echo is skipped, and a hint used, only while no
wide repaint has begun since; otherwise the event is planned like any other change.
…the answer

A full repaint parses the text, then waits for the workspace usage graph before it posts, which is
seconds when the graph is cold. It declared the rows in step with the text the moment it had
parsed, so an edit made in that window took the narrow path and painted, and the rows that landed
after it were built before it: the edit was taken back off the screen, and the narrow repaints that
followed spliced into a table the text did not describe. Two full repaints in flight landed in
whichever order their waits ended, so the older text could be the one left showing.

The rows are now in step only once they land, and they land only if theirs is still the newest
repaint, the document version is unchanged, and the view has not painted or put its tree out of
step since; a newer repaint replaces them, and anything else reads the text again. The count is
kept per view, because two tabs of one document share its tree and re-read each other for ever
when the rule asked whether the tree was still their own.
… repaint

A rename re-keys the entry's run of rows, and the edit paints that run under the new name before
it writes. The echo of the write still spliced its confirmation over the OLD id, from when the
echo was the paint: the table no longer held that id, answered the miss by asking for the whole
payload, and every rename ended in a full re-parse and repaint (about 1.4 s on a 47.8 MB
dictionary). A rename the document refused was taken back over the old id too, with the same
round trip.

The hint now carries the one id the model and the painted rows share, and taking a paint back
finds the entry by the id the file still spells and splices it over the run the paint left; once
a full repaint has begun since the paint, its rows are already the file's answer and only the
banner follows them.
…gn-namespace entry

The catalog names the definitions of Design Data and Architectural Data, and a name is unique only
within a namespace. The host's gate carried a rename of any top-level entry of a definition's name,
so renaming an Other Data or Configurations entry that shared it rewrote the definition in the
file, in a text dictionary and a binary one alike, and the architectural entry that really holds
the name read back unclassified.

The gate now asks the entry's section for its namespace through core's own section table, the
rule core applies when it classifies an entry and when it renames one. A test holds the host and
core to one answer, entry by entry, over a catalog with entries of a definition's name inside and
outside the namespace; the half outside is marked failing until the pinned core has the scope, and
passes against a build that does.
…after a reload, a revert or a dirty open

A mark says an entry differs from what the file holds, but the baseline was taken once, from the
document's text the first time it painted, and replaced only by a save. A file changed by another
tool and reloaded was marked Modified in a document with nothing to save; a revert left the marks
measured against the old text; and a document already dirty when the table opened, such as a
window restored with unsaved changes, was measured against its own edits and marked nothing.

The document now records each version known to read what the file says: clean when a view opens,
the dirty-state event that clears it on a save or a revert, and a reload, which VS Code reports
with no dirty-state event and is told by comparing the text with the file. The next full repaint
of that version takes the baseline from the text it parsed, and every tab repaints its marks. A
document opened dirty takes its baseline from the file's own bytes. An undo or redo back to a
clean document needs neither, since its text is the one the baseline was taken from.
…for the usage graph

Clearing the in-step flag when a full repaint began sent every keystroke in the text view, and
every undo, down the full re-parse for as long as the repaint waited for a cold usage graph, which
is after every save of a workspace file. The rows on screen are still the old ones in that window,
and a narrow repaint keeps them in step exactly as it did before; what must not happen is the full
repaint landing its older rows over it, and the landing rule already reads the text again when the
view has painted since it began. So the flag is left as it was until the rows land.

Also corrects the note on the held-back usage repost: its echo is no longer skipped after a full
repaint, so holding it back now saves a second full repaint rather than a lost edit.
…othing

A full repaint waits for the usage graph before it posts, and one whose text moved meanwhile reads
the document again. After the last tab had closed, that re-read registered a fresh tree of the
dictionary, tens of MB on a real one, which the close had just released and no tab was left to
release again; and a repaint that landed instead posted to the closed panel.

The view now knows it is closed, first thing on dispose, and a repaint begun or landing after that
does nothing, as do its narrow repaints and the check of its text against the file. The disk read
of a document opened dirty belongs to the document and still serves the tabs left open, but stops
before its parse once the last one has closed.
…as its baseline

A document opened with unsaved changes takes its baseline from the file's bytes, read
asynchronously. A save landing during that read was not baselined, because no repaint captures
while the read is pending, and the bytes that arrived afterwards, possibly from before the save,
then became the baseline: entries the save had just written showed as Modified in a clean
document.

A repaint now captures a saved text even while the read is pending, and the read's bytes count
only if the document has not turned clean since it began; if it has and nothing was baselined,
the file is read again. Also brings the baseline notes in slddBaseline.ts and the document state's
note up to date.
…g the whole file

A reload of a clean document says itself only by leaving the text equal to the file, so the first
change to a clean document was checked against the file, and the check read all of it, in every
tab, on the first edit after every save and every autosave. Measured in the test host on a
generated 48.7 MB dictionary: 14 to 45 ms and a buffer the size of the file per read, against
0.5 to 4 ms for a stat.

The document now keeps the file's size and modification time from the last moment it was known to
read what the file says, and the file is read only when they have moved since. A reload follows a
change on disk, so it is still found, and an edit leaves the stat where it was.
A new baseline now repaints every tab, and a change nothing narrow can plan repaints wide; either
could reach a tab whose webview was still loading and had no listener yet, so the full parse and
row build were thrown away ahead of the one its `ready` asks for anyway. A full repaint now waits
for the webview's first `ready`.
…nt is owed as a repair

A full repaint no longer clears the in-step flag when it begins, which is right for one that
repaints rows already in step (a save, a webview asking for its rows). It was not for one that
repairs rows known to be wrong: a change nothing narrow could plan, a narrow repaint that threw, a
paint the file never took. Until that repair landed, a following change could still be spliced
narrowly into the rows it was about to replace. The landing rule then read the text again, so
nothing wrong was left on screen, but the narrow claim was false in the meantime.

Those four repairs now put the view out of step before they begin.
# Conflicts:
#	src/host/SlddTextEditorProvider.ts
… core has it

The pinned core now classifies and renames by the System Composer catalog only in the Design
namespace, so the agreement over entries outside it is a plain test rather than one marked as
failing until the pin moved.
… from the table or the text

The panel is handed a node when a row is selected and builds its rows from it once. A JSON
dictionary's tree changes under it at every table edit, undo and redo, keystroke in the text view,
save, revert and reload, and the panel went on showing the node it was handed until another row
was clicked. The other two editors already said when their tree changed, and the panel's one
refresh answered them.

The JSON editor now says so too, after every repaint it posts, wide or narrow, and extension.ts
answers it with the same refresh. That is keystroke rate; the panel sends its page a node only
when it changes what the page shows, and a test pins that a keystroke in another entry sends it
nothing. Also stops a closed table being posted the banner that follows a failed edit's repair.
…one clean-at test

The reload check and the dirty open's disk baseline each wrapped the file read in
their own try/catch; readDisk now answers null on failure the way statDisk already
did. The "still clean at this version" test was spelled out three times (stampDisk
and both checkAgainstDisk guards); stillCleanAt names it once. Also: latestPost is
declared ahead of the post() that assigns it, noteClean's tail is one if, the
dirty-open branch gets braces like its neighbour, and the hintFor test drops the
rowId field HostEdit no longer has. No behaviour change.
@ww-mw
ww-mw merged commit e4039ce into main Oct 24, 2026
4 checks passed
@ww-mw
ww-mw deleted the json-sync-fixes branch October 24, 2026 03:21
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