@jxsuite/studio 5.5.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/chunks/{2020-g8mwpbkq.js → 2020-zvax9fmb.js} +3 -3
- package/dist/chunks/adapter-6zhjbpmd.js +4 -0
- package/dist/chunks/cf-account-picker-zfycs4k2.js +4 -0
- package/dist/chunks/cf-settings-fpa485q3.js +4 -0
- package/dist/chunks/components-b0vh4sq8.js +4 -0
- package/dist/chunks/content-source-z5t0d0h9.js +4 -0
- package/dist/chunks/convert-to-repeater-x19jr6c7.js +4 -0
- package/dist/chunks/editor-7e23n4a8.js +4 -0
- package/dist/chunks/entry-commands-5cm6aba2.js +4 -0
- package/dist/chunks/extension-sections-y90sa0n4.js +4 -0
- package/dist/chunks/file-ops-hv7gf757.js +4 -0
- package/dist/chunks/files-kb0epdxf.js +4 -0
- package/dist/chunks/format-host-eg1wy2bp.js +4 -0
- package/dist/chunks/fs-events-ky3vxfmh.js +4 -0
- package/dist/chunks/git-panel-yyg3bzp2.js +4 -0
- package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
- package/dist/chunks/grid-view-d9mybhn0.js +51 -0
- package/dist/chunks/iframe-host-9wdf0hes.js +4 -0
- package/dist/chunks/index-2tf0m2ne.js +4 -0
- package/dist/chunks/{index-arg188jq.js → index-vnwy9d44.js} +3 -3
- package/dist/chunks/{javascript-zcdmye2k.js → javascript-f0nrbdjb.js} +2 -2
- package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-nfa90dtf.js} +5 -5
- package/dist/chunks/jx-validate-7w11db9v.js +4 -0
- package/dist/chunks/layers-7zasrpse.js +4 -0
- package/dist/chunks/layers-panel-9nzc4fdq.js +4 -0
- package/dist/chunks/lit-html-5qr7nbng.js +4 -0
- package/dist/chunks/media-picker-maevn9d4.js +4 -0
- package/dist/chunks/menu-wq6q0v3y.js +4 -0
- package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-antn79jf.js} +3 -3
- package/dist/chunks/monaco-buffer-9ry2rn5x.js +4 -0
- package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-k8s40vwg.js} +3 -3
- package/dist/chunks/notify-g91gjd6w.js +4 -0
- package/dist/chunks/platform-jdwjp2vt.js +4 -0
- package/dist/chunks/publish-panel-8yym447p.js +4 -0
- package/dist/chunks/quick-search-9bzm9kh3.js +4 -0
- package/dist/chunks/references-znssfqvq.js +4 -0
- package/dist/chunks/right-panel-czbzktm8.js +4 -0
- package/dist/chunks/section-registry-0rtq73qs.js +4 -0
- package/dist/chunks/studio-011em5cf.js +5 -0
- package/dist/chunks/studio-02y9cb4h.js +6 -0
- package/dist/chunks/{studio-7gqyhjns.js → studio-09n3fyy0.js} +4 -4
- package/dist/chunks/{studio-xcng8nx7.js → studio-0c3fpdqj.js} +5 -5
- package/dist/chunks/studio-0mm38y62.js +5 -0
- package/dist/chunks/studio-0nm2skex.js +4 -0
- package/dist/chunks/{studio-73czgk6a.js → studio-153mwkyq.js} +4 -4
- package/dist/chunks/{studio-f8jyp45j.js → studio-1pxvgx1t.js} +4 -4
- package/dist/chunks/studio-27tc8q0h.js +5 -0
- package/dist/chunks/studio-28pz4s1k.js +5 -0
- package/dist/chunks/{studio-x5esgzct.js → studio-36s6qjgq.js} +4 -4
- package/dist/chunks/studio-4yztve2t.js +5 -0
- package/dist/chunks/{studio-x9sagq2d.js → studio-5fc0728f.js} +7 -7
- package/dist/chunks/{studio-7jazdy6y.js → studio-654fxcr0.js} +4 -4
- package/dist/chunks/studio-6exg5z4x.js +4 -0
- package/dist/chunks/studio-6k1eswp9.js +5 -0
- package/dist/chunks/{studio-5wfrgee9.js → studio-6zyt56ek.js} +4 -4
- package/dist/chunks/studio-7djymzt2.js +5 -0
- package/dist/chunks/{studio-b4epb26c.js → studio-7w04zw8h.js} +7 -7
- package/dist/chunks/studio-805fwp9k.js +5 -0
- package/dist/chunks/{studio-k1eaqyzm.js → studio-81twpmzj.js} +4 -4
- package/dist/chunks/studio-891t1tv9.js +5 -0
- package/dist/chunks/{studio-5n1km79v.js → studio-9n2j9nqk.js} +4 -4
- package/dist/chunks/{studio-he2fhrgp.js → studio-9tkaykt5.js} +4 -4
- package/dist/chunks/studio-ant1zbp3.js +5 -0
- package/dist/chunks/studio-b29h7g7s.js +5 -0
- package/dist/chunks/studio-b5rxwkj2.js +5 -0
- package/dist/chunks/studio-d007j0z3.js +7 -0
- package/dist/chunks/studio-dknr8m7b.js +14 -0
- package/dist/chunks/{studio-eq27rrr0.js → studio-dmymvk2j.js} +3 -3
- package/dist/chunks/studio-dtmdjmqb.js +5 -0
- package/dist/chunks/studio-eb5sty2k.js +5 -0
- package/dist/chunks/{studio-hc9q4j24.js → studio-ebt49j3c.js} +5 -5
- package/dist/chunks/studio-eegyf5vg.js +5 -0
- package/dist/chunks/studio-frv7vjhw.js +5 -0
- package/dist/chunks/studio-ft8pfpp0.js +5 -0
- package/dist/chunks/{studio-mhhc9btt.js → studio-gj4j9e7k.js} +4 -4
- package/dist/chunks/{studio-hf4qdjc8.js → studio-gxna6yta.js} +4 -4
- package/dist/chunks/studio-je15p5v8.js +6 -0
- package/dist/chunks/{studio-21enxdr8.js → studio-jsr14n1s.js} +4 -4
- package/dist/chunks/studio-k9pwvmz8.js +5 -0
- package/dist/chunks/{studio-p1eggxcf.js → studio-ks0ecgjx.js} +3 -3
- package/dist/chunks/studio-mb8jhtr4.js +64 -0
- package/dist/chunks/studio-mf41717j.js +5 -0
- package/dist/chunks/{studio-625ezz59.js → studio-mm8x32gg.js} +4 -4
- package/dist/chunks/{studio-pkkw32j0.js → studio-n2e133d6.js} +4 -4
- package/dist/chunks/{studio-c4pnt1c6.js → studio-njmg0e73.js} +5 -5
- package/dist/chunks/studio-nvykrgbj.js +4 -0
- package/dist/chunks/studio-nw4q9sdn.js +5 -0
- package/dist/chunks/studio-p6avneaq.js +5 -0
- package/dist/chunks/studio-pvj970st.js +5 -0
- package/dist/chunks/studio-r7yeeqc4.js +5 -0
- package/dist/chunks/studio-s85vah66.js +6 -0
- package/dist/chunks/studio-sadckcpp.js +7 -0
- package/dist/chunks/studio-sgzss898.js +9 -0
- package/dist/chunks/{studio-7by9a93k.js → studio-t0v47dpc.js} +4 -4
- package/dist/chunks/{studio-jnb285we.js → studio-tj28pxa1.js} +4 -4
- package/dist/chunks/{studio-6j2jn55a.js → studio-tzs6njnb.js} +8 -8
- package/dist/chunks/studio-vz7b43ng.js +5 -0
- package/dist/chunks/studio-w8w9frjy.js +16 -0
- package/dist/chunks/{studio-xc8ebsts.js → studio-wr0pq67n.js} +4 -4
- package/dist/chunks/{studio-8r4y2x0z.js → studio-y164brht.js} +4 -4
- package/dist/chunks/studio-y17505ge.js +928 -0
- package/dist/chunks/studio-ymsjnvp9.js +5 -0
- package/dist/chunks/studio-z1vdxq0e.js +5 -0
- package/dist/chunks/studio-z47mm077.js +10 -0
- package/dist/chunks/studio-zkkdbbvd.js +9 -0
- package/dist/chunks/{studio-awqghqxv.js → studio-zkvyd3ev.js} +4 -4
- package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-eq26t9gs.js} +5 -5
- package/dist/chunks/workspace-emt8454d.js +4 -0
- package/dist/chunks/ws-client-3gxqp06s.js +4 -0
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +62 -59
- package/dist/studio.css +1 -1
- package/dist/studio.js +115 -117
- package/package.json +5 -5
- package/src/account/action-flow.ts +294 -0
- package/src/account/action-required.ts +68 -0
- package/src/account-status.ts +29 -1
- package/src/browse/library-model.ts +3 -1
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +3 -1
- package/src/canvas/asset-resolve.ts +79 -1
- package/src/canvas/iframe-entry.ts +38 -1
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/files/asset-versions.ts +294 -0
- package/src/files/files.ts +61 -9
- package/src/files/fs-events.ts +279 -28
- package/src/files/media-meta.ts +2 -1
- package/src/files/media-paths.ts +5 -2
- package/src/files/media-upload.ts +5 -1
- package/src/new-project/add-repo-modal.ts +12 -1
- package/src/new-project/new-project-modal.ts +12 -1
- package/src/panels/git-panel.ts +25 -2
- package/src/platform-errors.ts +61 -1
- package/src/platforms/cloud.ts +389 -50
- package/src/platforms/devserver.ts +17 -1
- package/src/publish/pages-service.ts +17 -7
- package/src/services/ai-models.ts +30 -6
- package/src/services/ai-system-prompt.ts +81 -51
- package/src/settings/preferences-accounts.ts +42 -3
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/studio.ts +18 -1
- package/src/surfaces/dialog.ts +5 -0
- package/src/surfaces/welcome.json +86 -1
- package/src/surfaces/welcome.ts +61 -2
- package/src/types.ts +84 -2
- package/src/ui/ai-managed-connect.ts +41 -4
- package/src/ui/media-picker.ts +2 -1
- package/dist/chunks/adapter-bsbs78x9.js +0 -4
- package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
- package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
- package/dist/chunks/components-s51h3b7c.js +0 -4
- package/dist/chunks/content-source-ty3be0qg.js +0 -4
- package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
- package/dist/chunks/editor-t002b41m.js +0 -4
- package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
- package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
- package/dist/chunks/file-ops-78zeytkm.js +0 -4
- package/dist/chunks/files-gatzrz8v.js +0 -4
- package/dist/chunks/format-host-r59b1651.js +0 -4
- package/dist/chunks/fs-events-f8h91ndt.js +0 -4
- package/dist/chunks/git-panel-y12zf7nj.js +0 -4
- package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
- package/dist/chunks/grid-view-tafenfm2.css +0 -1
- package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
- package/dist/chunks/index-5aghyrfh.js +0 -4
- package/dist/chunks/jx-validate-653krkrf.js +0 -4
- package/dist/chunks/layers-32p8gtae.js +0 -4
- package/dist/chunks/layers-panel-rs58m105.js +0 -4
- package/dist/chunks/lit-html-54bmkwz3.js +0 -4
- package/dist/chunks/media-picker-5bb04wct.js +0 -4
- package/dist/chunks/menu-cbha7w2e.js +0 -4
- package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
- package/dist/chunks/notify-dc1nz5ar.js +0 -4
- package/dist/chunks/platform-2m49s1zf.js +0 -4
- package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
- package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
- package/dist/chunks/references-vx7mbj9f.js +0 -4
- package/dist/chunks/right-panel-7jzpbxja.js +0 -4
- package/dist/chunks/section-registry-gjryse41.js +0 -4
- package/dist/chunks/studio-3znfhcxg.js +0 -5
- package/dist/chunks/studio-58qam27v.js +0 -4
- package/dist/chunks/studio-6j59fwaw.js +0 -5
- package/dist/chunks/studio-7z69g1gx.js +0 -5
- package/dist/chunks/studio-93j3v6zx.js +0 -5
- package/dist/chunks/studio-9gf6v8zy.js +0 -5
- package/dist/chunks/studio-9rhmettf.js +0 -4
- package/dist/chunks/studio-ac5aq677.js +0 -10
- package/dist/chunks/studio-as65vbns.js +0 -5
- package/dist/chunks/studio-bkv0sga4.js +0 -6
- package/dist/chunks/studio-bttmqef4.js +0 -5
- package/dist/chunks/studio-c3y9d41k.js +0 -5
- package/dist/chunks/studio-ccegp7gz.js +0 -9
- package/dist/chunks/studio-chxabn98.js +0 -6
- package/dist/chunks/studio-cktcdsxz.js +0 -16
- package/dist/chunks/studio-cnspfke5.js +0 -5
- package/dist/chunks/studio-de3p1ktm.js +0 -5
- package/dist/chunks/studio-fsbcn530.js +0 -928
- package/dist/chunks/studio-gftga7a3.js +0 -64
- package/dist/chunks/studio-hf1rpcq0.js +0 -4
- package/dist/chunks/studio-hz02hq70.js +0 -5
- package/dist/chunks/studio-jsv3s3s1.js +0 -5
- package/dist/chunks/studio-ks7cksh0.js +0 -5
- package/dist/chunks/studio-nptx0dbs.js +0 -5
- package/dist/chunks/studio-pg0z7k98.js +0 -5
- package/dist/chunks/studio-pve0b2f2.js +0 -5
- package/dist/chunks/studio-q7zmjv3b.js +0 -6
- package/dist/chunks/studio-rpmvw7kj.js +0 -5
- package/dist/chunks/studio-rwhss52s.js +0 -5
- package/dist/chunks/studio-sbg3g3h0.js +0 -5
- package/dist/chunks/studio-sq1bn60g.js +0 -14
- package/dist/chunks/studio-vwz7rma2.js +0 -5
- package/dist/chunks/studio-wa55e8gf.js +0 -5
- package/dist/chunks/studio-x0mzdha2.js +0 -5
- package/dist/chunks/studio-xrgpakvc.js +0 -9
- package/dist/chunks/studio-xt3djk1w.js +0 -7
- package/dist/chunks/studio-yh3nggx1.js +0 -7
- package/dist/chunks/studio-zmecp6hq.js +0 -5
- package/dist/chunks/workspace-g6dvaete.js +0 -4
- package/dist/chunks/ws-client-shf41j5p.js +0 -4
|
@@ -49,6 +49,7 @@ import { DIALOG_COMMANDS, isDialog, POPOVER_COMMANDS } from "@jxsuite/schema/dia
|
|
|
49
49
|
import { isPopover } from "@jxsuite/schema/overlays";
|
|
50
50
|
import { localeDirection } from "@jxsuite/schema/locale";
|
|
51
51
|
import { getPlatform, hasPlatform } from "../platform";
|
|
52
|
+
import { assetVersionsEpoch, assetVersionsSnapshot } from "../files/asset-versions";
|
|
52
53
|
import type {
|
|
53
54
|
ApplyFormatIntent,
|
|
54
55
|
CanvasMode,
|
|
@@ -90,6 +91,12 @@ interface HostState {
|
|
|
90
91
|
canvasUrl: string;
|
|
91
92
|
ready: boolean;
|
|
92
93
|
pending: ParentToIframe | null;
|
|
94
|
+
/**
|
|
95
|
+
* The asset-version snapshot this frame last received (`assetVersions`), or null when it has
|
|
96
|
+
* received none since it last said `ready`. Compared by identity — the snapshot is memoized and
|
|
97
|
+
* changes identity only when a media version moves.
|
|
98
|
+
*/
|
|
99
|
+
postedVersions: Readonly<Record<string, string>> | null;
|
|
93
100
|
overlay: OverlayLayer;
|
|
94
101
|
/** Primary selected path (mirrors `session.selection`'s last entry), for hover de-dupe. */
|
|
95
102
|
selectionPath: (string | number)[] | null;
|
|
@@ -1304,6 +1311,8 @@ export function postPatchToHosts(forwardOps: WireDocOp[], tabId: string | null):
|
|
|
1304
1311
|
// Only the host that originated this edit already has the DOM the patch describes. A
|
|
1305
1312
|
// Split-view panel on the same document did NOT type it and must render normally.
|
|
1306
1313
|
const echoPaths = echoOrigin?.host === host ? echoOrigin.paths : undefined;
|
|
1314
|
+
// A patch draws nodes too, so it must not resolve media against an older map.
|
|
1315
|
+
syncAssetVersions(host);
|
|
1307
1316
|
host.channel.post(
|
|
1308
1317
|
echoPaths
|
|
1309
1318
|
? { echoPaths, forwardOps, gen, kind: "patch" }
|
|
@@ -1496,6 +1505,74 @@ function ensureSelectionWatch(): void {
|
|
|
1496
1505
|
selectionWatch = { stop: () => scope.stop() };
|
|
1497
1506
|
}
|
|
1498
1507
|
|
|
1508
|
+
/**
|
|
1509
|
+
* Post a ready frame the current asset-version snapshot if it holds an older one.
|
|
1510
|
+
*
|
|
1511
|
+
* Called before every render and patch the host sends, on `ready`, and whenever the versions move —
|
|
1512
|
+
* so a frame can never draw a node against a map older than the one Studio holds, and a version
|
|
1513
|
+
* that was forgotten (the bytes changed) never reaches a resolver after the forget. A frame that
|
|
1514
|
+
* has received nothing and would receive an empty map is sent nothing: the frame starts empty, and
|
|
1515
|
+
* a host that never versions anything (desktop, `jx dev`) should not pay a message per frame for
|
|
1516
|
+
* it.
|
|
1517
|
+
*/
|
|
1518
|
+
function syncAssetVersions(host: HostState): void {
|
|
1519
|
+
if (!host.ready) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const versions = assetVersionsSnapshot();
|
|
1523
|
+
if (versions === host.postedVersions) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (host.postedVersions === null && Object.keys(versions).length === 0) {
|
|
1527
|
+
host.postedVersions = versions;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
host.postedVersions = versions;
|
|
1531
|
+
host.channel.post({ kind: "assetVersions", versions });
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
let assetVersionsWatchStarted = false;
|
|
1535
|
+
let assetVersionsFlushQueued = false;
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Lazily start one watcher that reposts the asset versions to every ready frame when they move.
|
|
1539
|
+
*
|
|
1540
|
+
* Coalesced to a microtask: a Library walk notes one listing per directory, and each would
|
|
1541
|
+
* otherwise post the whole map. Coalescing cannot let a render see an older map, because every
|
|
1542
|
+
* render and patch syncs its own host first.
|
|
1543
|
+
*/
|
|
1544
|
+
function ensureAssetVersionsWatch(): void {
|
|
1545
|
+
if (assetVersionsWatchStarted) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
assetVersionsWatchStarted = true;
|
|
1549
|
+
const scope = effectScope(true);
|
|
1550
|
+
scope.run(() => {
|
|
1551
|
+
effect(() => {
|
|
1552
|
+
void assetVersionsEpoch.value;
|
|
1553
|
+
if (assetVersionsFlushQueued) {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
assetVersionsFlushQueued = true;
|
|
1557
|
+
queueMicrotask(() => {
|
|
1558
|
+
assetVersionsFlushQueued = false;
|
|
1559
|
+
publishAssetVersions();
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/** Bring every ready, connected frame up to the current asset versions. */
|
|
1566
|
+
export function publishAssetVersions(): void {
|
|
1567
|
+
for (const host of liveHosts) {
|
|
1568
|
+
if (!host.iframe.isConnected) {
|
|
1569
|
+
liveHosts.delete(host);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
syncAssetVersions(host);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1499
1576
|
/** Lazily start one reactive watcher that re-measures remote peers' selections in every host. */
|
|
1500
1577
|
let presenceWatchStarted = false;
|
|
1501
1578
|
let presenceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -1841,6 +1918,7 @@ function ensureHost(canvasEl: HTMLElement): HostState {
|
|
|
1841
1918
|
overlay,
|
|
1842
1919
|
panReqId: -1,
|
|
1843
1920
|
pending: null,
|
|
1921
|
+
postedVersions: null,
|
|
1844
1922
|
idle: null,
|
|
1845
1923
|
pendingEnterEdit: null,
|
|
1846
1924
|
pendingMeasures: new Map(),
|
|
@@ -1875,6 +1953,7 @@ function ensureHost(canvasEl: HTMLElement): HostState {
|
|
|
1875
1953
|
liveHosts.add(state);
|
|
1876
1954
|
ensureSelectionWatch();
|
|
1877
1955
|
ensurePresenceWatch();
|
|
1956
|
+
ensureAssetVersionsWatch();
|
|
1878
1957
|
return state;
|
|
1879
1958
|
}
|
|
1880
1959
|
|
|
@@ -1988,6 +2067,11 @@ function handleMessage(state: HostState, msg: IframeToParent): void {
|
|
|
1988
2067
|
if (table) {
|
|
1989
2068
|
state.channel.post({ chords: table.chords, kind: "keymap", mac: table.mac });
|
|
1990
2069
|
}
|
|
2070
|
+
/* Then the asset versions, also ahead of the render: the frame resolves media while it
|
|
2071
|
+
renders, and a FIFO channel makes "posted first" mean "known first". A frame that said
|
|
2072
|
+
`ready` again has rebooted and lost what it was sent before. */
|
|
2073
|
+
state.postedVersions = null;
|
|
2074
|
+
syncAssetVersions(state);
|
|
1991
2075
|
if (state.pending) {
|
|
1992
2076
|
state.channel.post(state.pending);
|
|
1993
2077
|
state.pending = null;
|
|
@@ -3088,6 +3172,7 @@ export async function mountIframeCanvas(
|
|
|
3088
3172
|
function deliverRender(state: HostState, message: ParentToIframe): void {
|
|
3089
3173
|
canvasPerf.hostRenderPosts += 1;
|
|
3090
3174
|
if (state.ready) {
|
|
3175
|
+
syncAssetVersions(state);
|
|
3091
3176
|
state.channel.post(message);
|
|
3092
3177
|
} else {
|
|
3093
3178
|
state.pending = message;
|
|
@@ -148,6 +148,19 @@ export type ParentToIframe =
|
|
|
148
148
|
dialogOpen?: (string | number)[] | null;
|
|
149
149
|
gen: number;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The content versions of the project's media, by normalized project path — what the frame folds
|
|
153
|
+
* into every render's `assets` context so a repo-space reference resolves to `<file>?v=<version>`
|
|
154
|
+
* (specs/studio.md §3.4). A path absent here resolves unversioned.
|
|
155
|
+
*
|
|
156
|
+
* A message of its own, not a field on `render`: the map is as large as the project's media and
|
|
157
|
+
* changes far less often than the document does. The host posts it before a frame's first render
|
|
158
|
+
* and again before any render or patch that would otherwise see an older map — the channel is
|
|
159
|
+
* FIFO, so "posted before" is "applied before". Gen-less and render-free: the frame keeps the
|
|
160
|
+
* latest map and re-installs its asset resolver around it, so the next node a patch draws already
|
|
161
|
+
* uses it. A frame built before this message existed ignores it and keeps resolving unversioned.
|
|
162
|
+
*/
|
|
163
|
+
| { kind: "assetVersions"; versions: Readonly<Record<string, string>> }
|
|
151
164
|
// Flip the forced color-scheme preview on the iframe root without re-rendering — a document-level
|
|
152
165
|
// Idempotent attribute write, deliberately gen-less (like endEdit).
|
|
153
166
|
| { kind: "setColorScheme"; scheme: "light" | "dark" | null }
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset versions — which project files the backend vouches for, and by what opaque version.
|
|
3
|
+
*
|
|
4
|
+
* A listing entry may carry `version` (protocol `DirEntry.version`): an opaque token for the file's
|
|
5
|
+
* bytes, equal tokens meaning identical bytes, ABSENT whenever the backend cannot vouch (an
|
|
6
|
+
* uncommitted, dirty or just-uploaded file). The cloud session sets it to the row's git blob sha.
|
|
7
|
+
* Studio appends it to a project-file URL as `?v=<version>` (`withVersion` in
|
|
8
|
+
* `canvas/asset-resolve.ts`), and a host that recognizes the version may answer that URL with an
|
|
9
|
+
* immutable cache lifetime — which is what lets a page full of images repaint from the browser
|
|
10
|
+
* cache instead of revalidating every one (specs/studio.md §3.4).
|
|
11
|
+
*
|
|
12
|
+
* That cache is only safe while the version is TRUE. A stale version names bytes the file no longer
|
|
13
|
+
* has, and an immutable response for it is never re-asked. So this map is built defensively:
|
|
14
|
+
*
|
|
15
|
+
* - **Listings feed it** ({@link beginListing} / {@link noteListing}). Every file entry of a listing
|
|
16
|
+
* either sets its version or, when the entry has none, deletes it — a listing that says nothing
|
|
17
|
+
* about a version is the backend withdrawing it.
|
|
18
|
+
* - **Change forgets** ({@link forgetVersions}). The file-event subscription forgets every path an
|
|
19
|
+
* event names, before the echo filter (Studio's own writes change bytes too), and
|
|
20
|
+
* `markLocalMutation` forgets the paths Studio is about to write.
|
|
21
|
+
* - **A listing never outlives a forget.** Nothing orders an HTTP response against the event socket,
|
|
22
|
+
* so a listing requested BEFORE a write can be answered AFTER the forget for it, carrying the old
|
|
23
|
+
* version. Each listing records the sequence number current when it began, every forget bumps the
|
|
24
|
+
* sequence and stamps the paths (and directory prefixes) it dirtied, and {@link noteListing}
|
|
25
|
+
* ignores any path dirtied after its listing began. The next listing, begun after the forget,
|
|
26
|
+
* decides.
|
|
27
|
+
*
|
|
28
|
+
* The canvas iframe receives {@link assetVersionsSnapshot} as one `assetVersions` message whenever
|
|
29
|
+
* {@link assetVersionsEpoch} moves, rather than a copy on every render. The snapshot holds MEDIA
|
|
30
|
+
* paths only — a `.json` or `.md` file is never loaded by URL from the canvas, and leaving them out
|
|
31
|
+
* keeps that message as small as the media set.
|
|
32
|
+
*
|
|
33
|
+
* Pure module state — it imports nothing but path normalization and the reactivity re-export — so
|
|
34
|
+
* the canvas host can import it without dragging in the file layer.
|
|
35
|
+
*
|
|
36
|
+
* @docs extending/embedding/platform-adapter
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { normalizeProjectPath } from "@jxsuite/schema/asset-paths";
|
|
40
|
+
import { shallowRef } from "../reactivity";
|
|
41
|
+
import type { DirEntry } from "../types";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extensions whose versions the canvas snapshot carries: images (svg included), fonts, video and
|
|
45
|
+
* audio — what a rendered document loads by URL. Documents and data (`.json`, `.md`, `.csv`) are
|
|
46
|
+
* deliberately absent.
|
|
47
|
+
*/
|
|
48
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
49
|
+
// Images
|
|
50
|
+
".apng",
|
|
51
|
+
".avif",
|
|
52
|
+
".bmp",
|
|
53
|
+
".gif",
|
|
54
|
+
".ico",
|
|
55
|
+
".jpeg",
|
|
56
|
+
".jpg",
|
|
57
|
+
".png",
|
|
58
|
+
".svg",
|
|
59
|
+
".webp",
|
|
60
|
+
// Fonts
|
|
61
|
+
".eot",
|
|
62
|
+
".otf",
|
|
63
|
+
".ttf",
|
|
64
|
+
".woff",
|
|
65
|
+
".woff2",
|
|
66
|
+
// Video
|
|
67
|
+
".m4v",
|
|
68
|
+
".mov",
|
|
69
|
+
".mp4",
|
|
70
|
+
".ogv",
|
|
71
|
+
".webm",
|
|
72
|
+
// Audio
|
|
73
|
+
".aac",
|
|
74
|
+
".flac",
|
|
75
|
+
".m4a",
|
|
76
|
+
".mp3",
|
|
77
|
+
".oga",
|
|
78
|
+
".ogg",
|
|
79
|
+
".opus",
|
|
80
|
+
".wav",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** Whether a project path is media the canvas loads by URL. */
|
|
84
|
+
export function isVersionedMediaPath(path: string): boolean {
|
|
85
|
+
const slash = path.lastIndexOf("/");
|
|
86
|
+
const dot = path.lastIndexOf(".");
|
|
87
|
+
return dot > slash + 1 && MEDIA_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Every version a listing has vouched for, keyed by normalized project path. */
|
|
91
|
+
const versions = new Map<string, string>();
|
|
92
|
+
/** Monotonic; bumped by every forget, read by every listing as it begins. */
|
|
93
|
+
let seq = 0;
|
|
94
|
+
/** Listings begun before this sequence number belong to a previous project and are ignored. */
|
|
95
|
+
let floor = 0;
|
|
96
|
+
/** The sequence number at which each path was last forgotten. */
|
|
97
|
+
const dirtiedPaths = new Map<string, number>();
|
|
98
|
+
/** The sequence number at which each directory prefix (`dir/`) was last forgotten. */
|
|
99
|
+
const dirtiedPrefixes = new Map<string, number>();
|
|
100
|
+
/** The memoized canvas snapshot, or null once a media version changed under it. */
|
|
101
|
+
let snapshot: Readonly<Record<string, string>> | null = null;
|
|
102
|
+
/**
|
|
103
|
+
* The one snapshot of a map with no media in it. Shared, so emptying an already-empty map keeps the
|
|
104
|
+
* snapshot's identity and the canvas host — which compares identities — posts nothing: a host that
|
|
105
|
+
* never versions anything (desktop, `jx dev`) never pays a message for it.
|
|
106
|
+
*/
|
|
107
|
+
const EMPTY_SNAPSHOT: Readonly<Record<string, string>> = Object.freeze({});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Bumped whenever the MEDIA versions change — the canvas host watches it to know when to repost
|
|
111
|
+
* {@link assetVersionsSnapshot}. A change to a non-media version does not move it: nothing the
|
|
112
|
+
* canvas holds would differ.
|
|
113
|
+
*/
|
|
114
|
+
export const assetVersionsEpoch = shallowRef(0);
|
|
115
|
+
|
|
116
|
+
function mediaChanged(): void {
|
|
117
|
+
snapshot = null;
|
|
118
|
+
assetVersionsEpoch.value += 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Begin a listing, returning the sequence number to pass to {@link noteListing} with its answer.
|
|
123
|
+
* Call it BEFORE the request goes out.
|
|
124
|
+
*/
|
|
125
|
+
export function beginListing(): number {
|
|
126
|
+
return seq;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether `path` was forgotten after sequence number `since`. */
|
|
130
|
+
function dirtiedSince(path: string, since: number): boolean {
|
|
131
|
+
const own = dirtiedPaths.get(path);
|
|
132
|
+
if (own !== undefined && own > since) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
for (const [prefix, at] of dirtiedPrefixes) {
|
|
136
|
+
if (at > since && path.startsWith(prefix)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Record what one listing said about its files' versions.
|
|
145
|
+
*
|
|
146
|
+
* A file entry WITH a version sets it; one without deletes any version held, since the backend no
|
|
147
|
+
* longer vouches for those bytes. Directories are ignored. A path forgotten after `since` (the
|
|
148
|
+
* value {@link beginListing} returned when this listing began) is skipped entirely: the listing may
|
|
149
|
+
* predate the change that forgot it.
|
|
150
|
+
*/
|
|
151
|
+
export function noteListing(entries: readonly DirEntry[], since: number): void {
|
|
152
|
+
if (since < floor) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let changed = false;
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (entry.type !== "file") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const path = normalizeProjectPath(entry.path);
|
|
161
|
+
if (path === "" || dirtiedSince(path, since)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const version =
|
|
165
|
+
typeof entry.version === "string" && entry.version !== "" ? entry.version : null;
|
|
166
|
+
const held = versions.get(path);
|
|
167
|
+
if (version === null) {
|
|
168
|
+
if (held === undefined) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
versions.delete(path);
|
|
172
|
+
} else if (held === version) {
|
|
173
|
+
continue;
|
|
174
|
+
} else {
|
|
175
|
+
versions.set(path, version);
|
|
176
|
+
}
|
|
177
|
+
if (isVersionedMediaPath(path)) {
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (changed) {
|
|
182
|
+
mediaChanged();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List through `read` with the listing's versions noted — {@link beginListing} before the request,
|
|
188
|
+
* {@link noteListing} with its answer. Resolves (or rejects) with exactly what `read` did.
|
|
189
|
+
*/
|
|
190
|
+
export async function notedListing<T extends readonly DirEntry[]>(
|
|
191
|
+
read: () => Promise<T>,
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
const since = beginListing();
|
|
194
|
+
const entries = await read();
|
|
195
|
+
noteListing(entries, since);
|
|
196
|
+
return entries;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Forget the versions of `paths`, and of every file under each of `prefixDirs`, and make sure no
|
|
201
|
+
* listing that began before this call can restore them.
|
|
202
|
+
*/
|
|
203
|
+
export function forgetVersions(paths: readonly string[], prefixDirs: readonly string[] = []): void {
|
|
204
|
+
seq += 1;
|
|
205
|
+
let changed = false;
|
|
206
|
+
for (const raw of paths) {
|
|
207
|
+
const path = normalizeProjectPath(raw);
|
|
208
|
+
if (path === "") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
dirtiedPaths.set(path, seq);
|
|
212
|
+
if (versions.delete(path) && isVersionedMediaPath(path)) {
|
|
213
|
+
changed = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const raw of prefixDirs) {
|
|
217
|
+
const dir = normalizeProjectPath(raw);
|
|
218
|
+
if (dir === "") {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const prefix = `${dir}/`;
|
|
222
|
+
dirtiedPrefixes.set(prefix, seq);
|
|
223
|
+
for (const path of versions.keys()) {
|
|
224
|
+
if (path.startsWith(prefix)) {
|
|
225
|
+
versions.delete(path);
|
|
226
|
+
if (isVersionedMediaPath(path)) {
|
|
227
|
+
changed = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (changed) {
|
|
233
|
+
mediaChanged();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The version a listing vouched for `path`, or undefined when none is held. */
|
|
238
|
+
export function versionOf(path: string): string | undefined {
|
|
239
|
+
return versions.get(normalizeProjectPath(path));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Every media path's version as a plain record, for the canvas iframe. Memoized: the same object is
|
|
244
|
+
* returned until a media version changes, so identity is a cheap "did anything move" test.
|
|
245
|
+
*/
|
|
246
|
+
export function assetVersionsSnapshot(): Readonly<Record<string, string>> {
|
|
247
|
+
if (snapshot) {
|
|
248
|
+
return snapshot;
|
|
249
|
+
}
|
|
250
|
+
const out: Record<string, string> = {};
|
|
251
|
+
let any = false;
|
|
252
|
+
for (const [path, version] of versions) {
|
|
253
|
+
if (isVersionedMediaPath(path)) {
|
|
254
|
+
out[path] = version;
|
|
255
|
+
any = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
snapshot = any ? Object.freeze(out) : EMPTY_SNAPSHOT;
|
|
259
|
+
return snapshot;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Forget EVERY version, and ignore every listing begun before this call when it lands.
|
|
264
|
+
*
|
|
265
|
+
* For a gap in the event stream: a reconnect, or the stream's first open after listings were
|
|
266
|
+
* already noted (§9.2). The events that would have forgotten individual paths are gone, and the
|
|
267
|
+
* versions at risk are not only the file tree's — the Library walk, the media picker and media
|
|
268
|
+
* metadata note directories the tree never loaded, and a re-list of the tree cannot reach them. A
|
|
269
|
+
* version Studio can no longer vouch for has to go, because an immutable response for it is never
|
|
270
|
+
* re-asked. Listings begun after this call vouch afresh.
|
|
271
|
+
*
|
|
272
|
+
* Raising the floor, rather than stamping paths, is what covers a listing that was in flight across
|
|
273
|
+
* the gap: it has no forget recorded against it, and would otherwise put the old version back.
|
|
274
|
+
*/
|
|
275
|
+
export function forgetAllVersions(): void {
|
|
276
|
+
const hadMedia = assetVersionsSnapshot() !== EMPTY_SNAPSHOT;
|
|
277
|
+
versions.clear();
|
|
278
|
+
// Every record is older than the new floor, and every listing it could have guarded is below it.
|
|
279
|
+
dirtiedPaths.clear();
|
|
280
|
+
dirtiedPrefixes.clear();
|
|
281
|
+
seq += 1;
|
|
282
|
+
floor = seq;
|
|
283
|
+
if (hadMedia) {
|
|
284
|
+
mediaChanged();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Drop every version and dirt record — a project switch, and between tests. A listing begun before
|
|
290
|
+
* the reset is ignored when it lands: it lists the project that was closed.
|
|
291
|
+
*/
|
|
292
|
+
export function resetAssetVersions(): void {
|
|
293
|
+
forgetAllVersions();
|
|
294
|
+
}
|
package/src/files/files.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { ensureDependenciesInstalled } from "../packages/ensure-deps";
|
|
|
47
47
|
import { maybePromptJxsuiteUpdate } from "../packages/jxsuite-update";
|
|
48
48
|
import { autoSyncProjectOnOpen } from "../packages/pull-package-sync";
|
|
49
49
|
import { markLocalMutation } from "./fs-events";
|
|
50
|
+
import { notedListing, resetAssetVersions } from "./asset-versions";
|
|
50
51
|
import { ensureIgnoreLayers, isIgnoredEntry, resetIgnoreCache } from "./gitignore";
|
|
51
52
|
import { SETTINGS } from "../services/settings/definitions";
|
|
52
53
|
import { readStoredSetting, setSetting } from "../services/settings/kernel";
|
|
@@ -78,6 +79,8 @@ import { runActiveReported, runReported } from "../commands/run-reported";
|
|
|
78
79
|
import { collectionOfPath } from "../content/entry-model";
|
|
79
80
|
import { confirmFileDelete, parseSourceForPath, renamePromptMessage } from "./file-ops";
|
|
80
81
|
import { parseJsonDocument } from "@jxsuite/schema/json-layout";
|
|
82
|
+
// The pure half of the differ: order-insensitive equality with no Yjs behind it.
|
|
83
|
+
import { deepEqual } from "@jxsuite/collab/diff-core";
|
|
81
84
|
import type { JsonLayout } from "@jxsuite/schema/json-layout";
|
|
82
85
|
import { invalidateUsages } from "../services/references";
|
|
83
86
|
import {
|
|
@@ -123,7 +126,8 @@ export async function loadDirectory(dirPath: string) {
|
|
|
123
126
|
ignore rules synchronously while it builds rows, and a repaint that beat the rules would draw
|
|
124
127
|
a `node_modules` and then take it away again. Concurrent, because neither needs the other. */
|
|
125
128
|
const [entries] = await Promise.all([
|
|
126
|
-
|
|
129
|
+
// Noted: a listing is where Studio learns which files' bytes the backend vouches for.
|
|
130
|
+
notedListing(() => platform.listDirectory(dirPath)),
|
|
127
131
|
ensureIgnoreLayers(dirPath),
|
|
128
132
|
]);
|
|
129
133
|
projectState.dirs.set(dirPath, entries);
|
|
@@ -161,6 +165,7 @@ export async function loadProject() {
|
|
|
161
165
|
void loadFormats();
|
|
162
166
|
refreshExtensionUi(platform);
|
|
163
167
|
resetIgnoreCache();
|
|
168
|
+
resetAssetVersions();
|
|
164
169
|
|
|
165
170
|
setProjectState({
|
|
166
171
|
dirs: new Map(),
|
|
@@ -241,6 +246,7 @@ export async function openProject({
|
|
|
241
246
|
void loadFormats();
|
|
242
247
|
refreshExtensionUi(platform);
|
|
243
248
|
resetIgnoreCache();
|
|
249
|
+
resetAssetVersions();
|
|
244
250
|
|
|
245
251
|
setProjectState({
|
|
246
252
|
.../** @type {ProjectState} */ projectState,
|
|
@@ -2431,12 +2437,6 @@ export async function openFileInPane(paneId: string, path: string): Promise<void
|
|
|
2431
2437
|
await openFileInTab(path, { focus: false, paneId, preview: true });
|
|
2432
2438
|
}
|
|
2433
2439
|
|
|
2434
|
-
/**
|
|
2435
|
-
* Reload an already-open tab from disk without changing the active tab. Used to refresh after AI
|
|
2436
|
-
* assistant writes to a file.
|
|
2437
|
-
*
|
|
2438
|
-
* @param {string} path
|
|
2439
|
-
*/
|
|
2440
2440
|
/** Reload an open tab from disk when an external change arrives — but only if it is not dirty. */
|
|
2441
2441
|
export function reloadCleanTab(path: string): void {
|
|
2442
2442
|
// Co-edited docs never reload from disk: the shared Y.Doc is ahead of the provider's write-back
|
|
@@ -2452,25 +2452,77 @@ export function reloadCleanTab(path: string): void {
|
|
|
2452
2452
|
}
|
|
2453
2453
|
}
|
|
2454
2454
|
|
|
2455
|
-
|
|
2455
|
+
/**
|
|
2456
|
+
* Re-read open tabs after the file-event stream reconnected (`FsSyncContext.onResyncContent`).
|
|
2457
|
+
*
|
|
2458
|
+
* The paths arrive already filtered to clean, non-collab tabs. Each reload is `onlyIfChanged`
|
|
2459
|
+
* because a reconnect almost always changed nothing: an unconditional reload would replace every
|
|
2460
|
+
* open document with an identical one, which is a repaint per tab and an undo boundary nobody
|
|
2461
|
+
* made.
|
|
2462
|
+
*/
|
|
2463
|
+
export function reloadTabsIfChanged(paths: string[]): void {
|
|
2464
|
+
for (const path of paths) {
|
|
2465
|
+
void reloadFileInTab(path, { onlyIfChanged: true });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* Reload an already-open tab from disk without changing the active tab. Used to refresh after AI
|
|
2471
|
+
* assistant writes to a file, an external change, or a reconnect.
|
|
2472
|
+
*
|
|
2473
|
+
* With `onlyIfChanged`, a file whose parsed document (and frontmatter) equals what the tab already
|
|
2474
|
+
* holds is left alone: no assignment, so no re-render and nothing for undo to see. A JSON file's
|
|
2475
|
+
* layout is still taken from the new text, because a file reformatted on disk with the same content
|
|
2476
|
+
* would otherwise be written back in its old layout on the next save — and the layout renders
|
|
2477
|
+
* nothing.
|
|
2478
|
+
*
|
|
2479
|
+
* Whether to reload is decided about the tab as it was when the read STARTED, and the read is a
|
|
2480
|
+
* round trip (a network one on cloud). So the decision is re-checked after every await and before
|
|
2481
|
+
* anything is assigned: a tab that was clean and took an edit in the meantime, or that joined a
|
|
2482
|
+
* co-editing session, keeps what it now holds. Otherwise the disk copy would land over the author's
|
|
2483
|
+
* newest keystrokes and clear `dirty` — work lost without a trace — and a reconnect resync, which
|
|
2484
|
+
* re-reads every clean tab at the moment the author comes back to the window, is exactly when those
|
|
2485
|
+
* keystrokes arrive. A tab that was ALREADY dirty when the read started is the caller asking for it
|
|
2486
|
+
* to be replaced (an AI write, a rename's reference rewrite), and keeps that meaning.
|
|
2487
|
+
*/
|
|
2488
|
+
export async function reloadFileInTab(path: string, opts: { onlyIfChanged?: boolean } = {}) {
|
|
2456
2489
|
for (const [, tab] of workspace.tabs.entries()) {
|
|
2457
2490
|
if (tab.documentPath === path) {
|
|
2458
2491
|
const platform = getPlatform();
|
|
2492
|
+
const wasDirty = tab.doc.dirty;
|
|
2493
|
+
const overtaken = () => (!wasDirty && tab.doc.dirty) || isCollabPath(path);
|
|
2459
2494
|
try {
|
|
2460
2495
|
const content = await platform.readFile(path);
|
|
2461
2496
|
if (!content) {
|
|
2462
2497
|
return;
|
|
2463
2498
|
}
|
|
2464
2499
|
await loadFormats();
|
|
2500
|
+
// Everything below is synchronous except the format parse, which checks again after it.
|
|
2501
|
+
if (overtaken()) {
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2465
2504
|
if (formatForPath(path)) {
|
|
2466
2505
|
const { document, frontmatter } = await parseSourceForPath(path, content);
|
|
2506
|
+
if (overtaken()) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
if (
|
|
2510
|
+
opts.onlyIfChanged &&
|
|
2511
|
+
deepEqual(document, tab.doc.document) &&
|
|
2512
|
+
deepEqual(frontmatter, tab.doc.content.frontmatter)
|
|
2513
|
+
) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2467
2516
|
tab.doc.document = document;
|
|
2468
2517
|
tab.doc.content.frontmatter = frontmatter;
|
|
2469
2518
|
} else if (path.endsWith(".json")) {
|
|
2470
2519
|
// The file changed under the tab, so its layout is whatever the new text says.
|
|
2471
2520
|
const parsed = parseJsonDocument(content);
|
|
2472
|
-
tab.doc.document = parsed.document as JxMutableNode;
|
|
2473
2521
|
tab.doc.layout = parsed.layout;
|
|
2522
|
+
if (opts.onlyIfChanged && deepEqual(parsed.document, tab.doc.document)) {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
tab.doc.document = parsed.document as JxMutableNode;
|
|
2474
2526
|
}
|
|
2475
2527
|
tab.doc.dirty = false;
|
|
2476
2528
|
} catch (error) {
|