@jxsuite/studio 2.4.3 → 4.0.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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
|
@@ -0,0 +1,3163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
commitTabBuffers
|
|
3
|
+
} from "./studio-wcy8trfx.js";
|
|
4
|
+
import {
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
effectScope,
|
|
8
|
+
notify,
|
|
9
|
+
onScopeDispose,
|
|
10
|
+
reactive,
|
|
11
|
+
toRaw
|
|
12
|
+
} from "./studio-xrapxwwc.js";
|
|
13
|
+
import {
|
|
14
|
+
applyDocOpToDoc,
|
|
15
|
+
childArray,
|
|
16
|
+
cloneValue
|
|
17
|
+
} from "./studio-5ehhackm.js";
|
|
18
|
+
import {
|
|
19
|
+
childIndex,
|
|
20
|
+
getNodeAtPath,
|
|
21
|
+
isAncestor,
|
|
22
|
+
normalizeArrayChildren,
|
|
23
|
+
parentElementPath,
|
|
24
|
+
pathsEqual,
|
|
25
|
+
projectState
|
|
26
|
+
} from "./studio-p4ngc046.js";
|
|
27
|
+
import {
|
|
28
|
+
ensureNestedStyle,
|
|
29
|
+
getNestedStyle,
|
|
30
|
+
isEventBinding
|
|
31
|
+
} from "./studio-q7v10cg2.js";
|
|
32
|
+
import {
|
|
33
|
+
defaultContentFormat,
|
|
34
|
+
formatByName,
|
|
35
|
+
formatForPath
|
|
36
|
+
} from "./studio-eenfkt4e.js";
|
|
37
|
+
import {
|
|
38
|
+
getPlatform
|
|
39
|
+
} from "./studio-9ab8gqsm.js";
|
|
40
|
+
import {
|
|
41
|
+
__require
|
|
42
|
+
} from "./studio-a0tswy0w.js";
|
|
43
|
+
|
|
44
|
+
// ../schema/src/locale.ts
|
|
45
|
+
var RTL_SCRIPTS = new Set([
|
|
46
|
+
"Adlm",
|
|
47
|
+
"Arab",
|
|
48
|
+
"Aran",
|
|
49
|
+
"Armi",
|
|
50
|
+
"Avst",
|
|
51
|
+
"Cprt",
|
|
52
|
+
"Egyd",
|
|
53
|
+
"Egyh",
|
|
54
|
+
"Elym",
|
|
55
|
+
"Gara",
|
|
56
|
+
"Hatr",
|
|
57
|
+
"Hebr",
|
|
58
|
+
"Hluw",
|
|
59
|
+
"Hung",
|
|
60
|
+
"Khar",
|
|
61
|
+
"Lydi",
|
|
62
|
+
"Mand",
|
|
63
|
+
"Mani",
|
|
64
|
+
"Mend",
|
|
65
|
+
"Merc",
|
|
66
|
+
"Mero",
|
|
67
|
+
"Narb",
|
|
68
|
+
"Nbat",
|
|
69
|
+
"Nkoo",
|
|
70
|
+
"Orkh",
|
|
71
|
+
"Palm",
|
|
72
|
+
"Phli",
|
|
73
|
+
"Phlp",
|
|
74
|
+
"Phnx",
|
|
75
|
+
"Prti",
|
|
76
|
+
"Rohg",
|
|
77
|
+
"Samr",
|
|
78
|
+
"Sarb",
|
|
79
|
+
"Sogd",
|
|
80
|
+
"Sogo",
|
|
81
|
+
"Syrc",
|
|
82
|
+
"Thaa",
|
|
83
|
+
"Todr",
|
|
84
|
+
"Yezi"
|
|
85
|
+
]);
|
|
86
|
+
var LANGUAGE_TAG_PATTERN = "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$";
|
|
87
|
+
function canonicalizeLocale(tag) {
|
|
88
|
+
if (typeof tag !== "string" || tag.trim() === "") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return new Intl.Locale(tag.trim()).toString();
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isWellFormedLocale(tag) {
|
|
98
|
+
return canonicalizeLocale(tag) !== null;
|
|
99
|
+
}
|
|
100
|
+
function localeDirection(tag) {
|
|
101
|
+
const canonical = canonicalizeLocale(tag);
|
|
102
|
+
if (canonical === null) {
|
|
103
|
+
return "ltr";
|
|
104
|
+
}
|
|
105
|
+
const locale = new Intl.Locale(canonical);
|
|
106
|
+
const script = locale.script ?? locale.maximize().script;
|
|
107
|
+
return script !== undefined && RTL_SCRIPTS.has(script) ? "rtl" : "ltr";
|
|
108
|
+
}
|
|
109
|
+
function localeLabel(tag) {
|
|
110
|
+
const canonical = canonicalizeLocale(tag);
|
|
111
|
+
if (canonical === null) {
|
|
112
|
+
return typeof tag === "string" ? tag : "";
|
|
113
|
+
}
|
|
114
|
+
return new Intl.DisplayNames([canonical], { type: "language" }).of(canonical) ?? canonical;
|
|
115
|
+
}
|
|
116
|
+
var DEFAULT_ROUTING = "prefix-except-default";
|
|
117
|
+
function resolveI18n(projectConfig) {
|
|
118
|
+
const raw = projectConfig.i18n;
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (!raw || typeof raw !== "object") {
|
|
121
|
+
return { errors, i18n: null };
|
|
122
|
+
}
|
|
123
|
+
const declared = Array.isArray(raw.locales) ? raw.locales : [];
|
|
124
|
+
const locales = [];
|
|
125
|
+
for (const tag of declared) {
|
|
126
|
+
const canonical = canonicalizeLocale(tag);
|
|
127
|
+
if (canonical === null) {
|
|
128
|
+
errors.push(`i18n.locales: "${String(tag)}" is not a well-formed BCP 47 language tag.`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!locales.includes(canonical)) {
|
|
132
|
+
locales.push(canonical);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const defaultLocale = canonicalizeLocale(raw.defaultLocale) ?? locales[0] ?? null;
|
|
136
|
+
if (raw.defaultLocale !== undefined && canonicalizeLocale(raw.defaultLocale) === null) {
|
|
137
|
+
errors.push(`i18n.defaultLocale: "${String(raw.defaultLocale)}" is not a well-formed BCP 47 language tag.`);
|
|
138
|
+
}
|
|
139
|
+
if (defaultLocale === null) {
|
|
140
|
+
errors.push("i18n is declared with no usable locale — set `defaultLocale` or `locales`.");
|
|
141
|
+
return { errors, i18n: null };
|
|
142
|
+
}
|
|
143
|
+
if (!locales.includes(defaultLocale)) {
|
|
144
|
+
locales.unshift(defaultLocale);
|
|
145
|
+
}
|
|
146
|
+
const routing = raw.routing === "prefix-always" || raw.routing === "prefix-except-default" ? raw.routing : DEFAULT_ROUTING;
|
|
147
|
+
return { errors, i18n: { defaultLocale, locales, routing } };
|
|
148
|
+
}
|
|
149
|
+
function localeOfPath(path, i18n) {
|
|
150
|
+
const index = localeSegment(pathSegments(path), i18n);
|
|
151
|
+
if (index === -1 || i18n === null) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return canonicalizeLocale(pathSegments(path)[index]);
|
|
155
|
+
}
|
|
156
|
+
function servedLocaleOfPath(path, i18n) {
|
|
157
|
+
const own = localeOfPath(path, i18n);
|
|
158
|
+
if (own !== null || i18n === null || i18n.routing !== "prefix-except-default") {
|
|
159
|
+
return own;
|
|
160
|
+
}
|
|
161
|
+
return pathSegments(path)[0] === "pages" ? i18n.defaultLocale : null;
|
|
162
|
+
}
|
|
163
|
+
function translationKeyOfPath(path, i18n) {
|
|
164
|
+
const segments = pathSegments(path);
|
|
165
|
+
const index = localeSegment(segments, i18n);
|
|
166
|
+
return index === -1 ? segments.join("/") : segments.filter((_, i) => i !== index).join("/");
|
|
167
|
+
}
|
|
168
|
+
function translationPathFor(path, locale, i18n) {
|
|
169
|
+
if (i18n === null) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const canonical = canonicalizeLocale(locale);
|
|
173
|
+
if (canonical === null || !i18n.locales.includes(canonical)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const segments = pathSegments(path);
|
|
177
|
+
const existing = localeSegment(segments, i18n);
|
|
178
|
+
const slot = existing === -1 ? localeSlot(segments) : existing;
|
|
179
|
+
if (slot === -1) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const bare = existing === -1 ? segments : segments.filter((_, i) => i !== existing);
|
|
183
|
+
if (canonical === i18n.defaultLocale && i18n.routing === "prefix-except-default") {
|
|
184
|
+
return bare.join("/");
|
|
185
|
+
}
|
|
186
|
+
return bare.toSpliced(slot, 0, canonical.toLowerCase()).join("/");
|
|
187
|
+
}
|
|
188
|
+
function pathSegments(path) {
|
|
189
|
+
return path.replace(/^\.\//, "").split("/").filter(Boolean);
|
|
190
|
+
}
|
|
191
|
+
function localeSegment(segments, i18n) {
|
|
192
|
+
if (i18n === null) {
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
return segments.findIndex((segment) => {
|
|
196
|
+
const canonical = canonicalizeLocale(segment);
|
|
197
|
+
return canonical !== null && i18n.locales.includes(canonical);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function localeSlot(segments) {
|
|
201
|
+
if (segments[0] === "pages" && segments.length > 1) {
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (segments[0] === "content" && segments.length > 2) {
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
return -1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/content/collection-match.ts
|
|
211
|
+
var LOCALE_PLACEHOLDER = "{locale}";
|
|
212
|
+
function escapeLiteral(part) {
|
|
213
|
+
return part.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
214
|
+
}
|
|
215
|
+
function normalize(path) {
|
|
216
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
217
|
+
}
|
|
218
|
+
function configOf(config) {
|
|
219
|
+
return config ?? projectState?.projectConfig ?? null;
|
|
220
|
+
}
|
|
221
|
+
function contentSection(config) {
|
|
222
|
+
return config?.content ?? {};
|
|
223
|
+
}
|
|
224
|
+
function declaredLocales(config) {
|
|
225
|
+
const locales = resolveI18n(config ?? {}).i18n?.locales ?? [];
|
|
226
|
+
return locales.length > 0 ? [...locales] : null;
|
|
227
|
+
}
|
|
228
|
+
function isFileBacked(source) {
|
|
229
|
+
if (source.endsWith("/")) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const base = source.slice(source.lastIndexOf("/") + 1);
|
|
233
|
+
return /\.[a-z\d]+$/i.test(base);
|
|
234
|
+
}
|
|
235
|
+
function resolveExt(def) {
|
|
236
|
+
if (def.format === "json") {
|
|
237
|
+
return { ext: ".json", unresolvedFormat: null };
|
|
238
|
+
}
|
|
239
|
+
if (def.format === undefined || def.format === "") {
|
|
240
|
+
return { ext: defaultContentFormat()?.extensions[0] ?? ".json", unresolvedFormat: null };
|
|
241
|
+
}
|
|
242
|
+
const format = formatByName(def.format);
|
|
243
|
+
return format?.extensions[0] === undefined ? { ext: null, unresolvedFormat: def.format } : { ext: format.extensions[0], unresolvedFormat: null };
|
|
244
|
+
}
|
|
245
|
+
function sourcePattern(source, locales) {
|
|
246
|
+
const segment = locales === null ? "[^/]+" : `(?:${locales.map((locale) => escapeLiteral(locale)).join("|")})`;
|
|
247
|
+
const body = normalize(source).split(LOCALE_PLACEHOLDER).map((part) => escapeLiteral(part)).join(segment);
|
|
248
|
+
return new RegExp(`^${body}(?:/|$)`, "i");
|
|
249
|
+
}
|
|
250
|
+
function matchedRoot(source, target, locales) {
|
|
251
|
+
const hit = sourcePattern(source, locales).exec(target);
|
|
252
|
+
if (!hit) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return hit[0].replace(/\/$/, "");
|
|
256
|
+
}
|
|
257
|
+
function sourceWeight(source) {
|
|
258
|
+
return normalize(source).replaceAll(LOCALE_PLACEHOLDER, "").length;
|
|
259
|
+
}
|
|
260
|
+
function collectionForDirectory(dir, config) {
|
|
261
|
+
if (dir === null || dir === undefined || dir === "") {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const resolved = configOf(config);
|
|
265
|
+
const locales = declaredLocales(resolved);
|
|
266
|
+
const target = normalize(dir);
|
|
267
|
+
let best = null;
|
|
268
|
+
let bestWeight = -1;
|
|
269
|
+
for (const [name, def] of Object.entries(contentSection(resolved))) {
|
|
270
|
+
const { source } = def;
|
|
271
|
+
if (source === undefined || source === "" || isFileBacked(source)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const weight = sourceWeight(source);
|
|
275
|
+
if (weight <= bestWeight) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const root = matchedRoot(source, target, locales);
|
|
279
|
+
if (root === null) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
best = {
|
|
283
|
+
def,
|
|
284
|
+
dir: target,
|
|
285
|
+
fileBacked: false,
|
|
286
|
+
isSourceRoot: target.length === root.length,
|
|
287
|
+
name,
|
|
288
|
+
...resolveExt(def)
|
|
289
|
+
};
|
|
290
|
+
bestWeight = weight;
|
|
291
|
+
}
|
|
292
|
+
return best;
|
|
293
|
+
}
|
|
294
|
+
function collectionForFile(path, config) {
|
|
295
|
+
if (path === null || path === undefined || path === "") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const resolved = configOf(config);
|
|
299
|
+
const target = normalize(path);
|
|
300
|
+
const lower = target.toLowerCase();
|
|
301
|
+
for (const [name, def] of Object.entries(contentSection(resolved))) {
|
|
302
|
+
const { source } = def;
|
|
303
|
+
if (source === undefined || source === "" || !isFileBacked(source)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const src = normalize(source).toLowerCase();
|
|
307
|
+
if (lower === src || lower.endsWith(`/${src}`)) {
|
|
308
|
+
return {
|
|
309
|
+
def,
|
|
310
|
+
dir: target.includes("/") ? target.slice(0, target.lastIndexOf("/")) : ".",
|
|
311
|
+
fileBacked: true,
|
|
312
|
+
isSourceRoot: false,
|
|
313
|
+
name,
|
|
314
|
+
...resolveExt(def)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return collectionForDirectory(target.includes("/") ? target.slice(0, target.lastIndexOf("/")) : ".", resolved);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/utils/studio-utils.ts
|
|
322
|
+
function camelToKebab(str) {
|
|
323
|
+
return str.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
324
|
+
}
|
|
325
|
+
function camelToLabel(prop) {
|
|
326
|
+
return prop.replaceAll(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase());
|
|
327
|
+
}
|
|
328
|
+
function toCamelCase(str) {
|
|
329
|
+
return str.replaceAll(/[^a-zA-Z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
330
|
+
}
|
|
331
|
+
function kebabToLabel(val) {
|
|
332
|
+
return val.replaceAll(/(^|-)(\w)/g, (_, sep, c) => (sep ? " " : "") + c.toUpperCase());
|
|
333
|
+
}
|
|
334
|
+
function propLabel(entry, prop) {
|
|
335
|
+
return entry?.$label || camelToLabel(prop);
|
|
336
|
+
}
|
|
337
|
+
function attrLabel(entry, attr) {
|
|
338
|
+
if (entry?.$label) {
|
|
339
|
+
return entry.$label;
|
|
340
|
+
}
|
|
341
|
+
if (attr.includes("-")) {
|
|
342
|
+
return attr.replaceAll(/(^|-)(\w)/g, (_, sep, c) => (sep ? " " : "") + c.toUpperCase());
|
|
343
|
+
}
|
|
344
|
+
return camelToLabel(attr);
|
|
345
|
+
}
|
|
346
|
+
function abbreviateValue(val) {
|
|
347
|
+
const map = {
|
|
348
|
+
baseline: "base",
|
|
349
|
+
column: "col",
|
|
350
|
+
"column-reverse": "col-r",
|
|
351
|
+
contents: "cnt",
|
|
352
|
+
"flex-end": "end",
|
|
353
|
+
"flex-start": "start",
|
|
354
|
+
"flow-root": "flow",
|
|
355
|
+
inline: "inl",
|
|
356
|
+
"inline-block": "i-blk",
|
|
357
|
+
"inline-flex": "i-flx",
|
|
358
|
+
"inline-grid": "i-grd",
|
|
359
|
+
normal: "norm",
|
|
360
|
+
nowrap: "no-wr",
|
|
361
|
+
"row-reverse": "row-r",
|
|
362
|
+
"space-around": "arnd",
|
|
363
|
+
"space-between": "betw",
|
|
364
|
+
"space-evenly": "even",
|
|
365
|
+
stretch: "str",
|
|
366
|
+
"wrap-reverse": "wr-rev"
|
|
367
|
+
};
|
|
368
|
+
return map[val] || val;
|
|
369
|
+
}
|
|
370
|
+
function isMediaFormat(format) {
|
|
371
|
+
return format === "image" || format === "uri-reference";
|
|
372
|
+
}
|
|
373
|
+
function inferInputType(entry) {
|
|
374
|
+
if (entry.$shorthand === true) {
|
|
375
|
+
return "shorthand";
|
|
376
|
+
}
|
|
377
|
+
if (entry.$input === "button-group") {
|
|
378
|
+
return "button-group";
|
|
379
|
+
}
|
|
380
|
+
if (entry.$input === "media") {
|
|
381
|
+
return "media";
|
|
382
|
+
}
|
|
383
|
+
if (entry.format === "color") {
|
|
384
|
+
return "color";
|
|
385
|
+
}
|
|
386
|
+
if (isMediaFormat(entry.format)) {
|
|
387
|
+
return "media";
|
|
388
|
+
}
|
|
389
|
+
if (entry.$units !== undefined) {
|
|
390
|
+
return "number-unit";
|
|
391
|
+
}
|
|
392
|
+
if (entry.type === "number") {
|
|
393
|
+
return "number";
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(entry.enum)) {
|
|
396
|
+
return "select";
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(entry.examples) || Array.isArray(entry.presets)) {
|
|
399
|
+
return "combobox";
|
|
400
|
+
}
|
|
401
|
+
return "text";
|
|
402
|
+
}
|
|
403
|
+
function findContentTypeSchema(documentPath, projectConfig) {
|
|
404
|
+
if (!documentPath || !projectConfig?.content) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const match = collectionForFile(documentPath, projectConfig);
|
|
408
|
+
if (!match || !match.def.schema) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (!match.fileBacked) {
|
|
412
|
+
const ext = match.ext ?? defaultContentFormat()?.extensions[0] ?? ".json";
|
|
413
|
+
if (!documentPath.toLowerCase().endsWith(ext.toLowerCase())) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { name: match.name, schema: match.def.schema };
|
|
418
|
+
}
|
|
419
|
+
function friendlyNameToVar(name, prefix) {
|
|
420
|
+
const slug = name.trim().toLowerCase().replaceAll(/[^a-z0-9\s-]/g, "").replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "");
|
|
421
|
+
if (!slug) {
|
|
422
|
+
return "";
|
|
423
|
+
}
|
|
424
|
+
return `${prefix}${slug}`;
|
|
425
|
+
}
|
|
426
|
+
function varDisplayName(varName, prefix) {
|
|
427
|
+
return varName.replace(new RegExp(`^${prefix.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}`), "").replace(/^--/, "").replaceAll("-", " ").replaceAll(/\b\w/g, (c) => c.toUpperCase()) || varName;
|
|
428
|
+
}
|
|
429
|
+
function parseCemType(typeText) {
|
|
430
|
+
if (!typeText) {
|
|
431
|
+
return { kind: "text" };
|
|
432
|
+
}
|
|
433
|
+
const t = typeText.trim().replaceAll(/\s*\|\s*undefined\b/g, "").trim();
|
|
434
|
+
if (t === "boolean") {
|
|
435
|
+
return { kind: "boolean" };
|
|
436
|
+
}
|
|
437
|
+
if (t === "number") {
|
|
438
|
+
return { kind: "number" };
|
|
439
|
+
}
|
|
440
|
+
const enumMatch = t.match(/^'[^']*'(\s*\|\s*'[^']*')+$/);
|
|
441
|
+
if (enumMatch) {
|
|
442
|
+
const options = [...t.matchAll(/'([^']*)'/g)].map((m) => m[1]);
|
|
443
|
+
return { kind: "combobox", options };
|
|
444
|
+
}
|
|
445
|
+
return { kind: "text" };
|
|
446
|
+
}
|
|
447
|
+
function jsonClone(value) {
|
|
448
|
+
return JSON.parse(JSON.stringify(value));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/tabs/selection.ts
|
|
452
|
+
function primarySelection(selection) {
|
|
453
|
+
return selection && selection.length > 0 ? selection.at(-1) : null;
|
|
454
|
+
}
|
|
455
|
+
function selectionAnchor(selection) {
|
|
456
|
+
return selection && selection.length > 0 ? selection[0] : null;
|
|
457
|
+
}
|
|
458
|
+
function isSelected(selection, path) {
|
|
459
|
+
if (!path || !selection) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
return selection.some((p) => pathsEqual(p, path));
|
|
463
|
+
}
|
|
464
|
+
function cloneSelection(paths) {
|
|
465
|
+
return clonePaths(paths);
|
|
466
|
+
}
|
|
467
|
+
function clonePaths(paths) {
|
|
468
|
+
const out = [];
|
|
469
|
+
for (const path of paths) {
|
|
470
|
+
out.push([...path]);
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
function toggleSelected(selection, path) {
|
|
475
|
+
if (isSelected(selection, path)) {
|
|
476
|
+
return clonePaths(selection.filter((p) => !pathsEqual(p, path)));
|
|
477
|
+
}
|
|
478
|
+
return [...clonePaths(selection), [...path]];
|
|
479
|
+
}
|
|
480
|
+
function rangeSelection(rows, anchor, target) {
|
|
481
|
+
const to = rows.findIndex((p) => pathsEqual(p, target));
|
|
482
|
+
const from = anchor ? rows.findIndex((p) => pathsEqual(p, anchor)) : -1;
|
|
483
|
+
if (to === -1 || from === -1) {
|
|
484
|
+
return [[...target]];
|
|
485
|
+
}
|
|
486
|
+
const slice = from <= to ? rows.slice(from, to + 1) : rows.slice(to, from + 1).toReversed();
|
|
487
|
+
return clonePaths(slice);
|
|
488
|
+
}
|
|
489
|
+
function pruneSelection(selection, removed) {
|
|
490
|
+
return clonePaths(selection.filter((p) => !isAncestor(removed, p)));
|
|
491
|
+
}
|
|
492
|
+
function comparePaths(a, b) {
|
|
493
|
+
const len = Math.min(a.length, b.length);
|
|
494
|
+
for (let i = 0;i < len; i++) {
|
|
495
|
+
const x = a[i];
|
|
496
|
+
const y = b[i];
|
|
497
|
+
if (x === y) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
501
|
+
return x - y;
|
|
502
|
+
}
|
|
503
|
+
return String(x) < String(y) ? -1 : 1;
|
|
504
|
+
}
|
|
505
|
+
return a.length - b.length;
|
|
506
|
+
}
|
|
507
|
+
function topLevelSelection(selection) {
|
|
508
|
+
return clonePaths(selection.filter((p) => !selection.some((q) => !pathsEqual(q, p) && isAncestor(q, p))));
|
|
509
|
+
}
|
|
510
|
+
function isSpliceablePath(path) {
|
|
511
|
+
return path.at(-2) === "children" && typeof path.at(-1) === "number";
|
|
512
|
+
}
|
|
513
|
+
function structuralBatch(selection) {
|
|
514
|
+
return topLevelSelection(selection).filter((path) => isSpliceablePath(path)).toSorted((a, b) => comparePaths(b, a));
|
|
515
|
+
}
|
|
516
|
+
function uniquePaths(paths) {
|
|
517
|
+
const out = [];
|
|
518
|
+
for (const path of paths) {
|
|
519
|
+
if (!out.some((p) => pathsEqual(p, path))) {
|
|
520
|
+
out.push([...path]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
function unifyValues(values) {
|
|
526
|
+
if (values.length === 0) {
|
|
527
|
+
return { mixed: false, value: undefined };
|
|
528
|
+
}
|
|
529
|
+
const first = values[0];
|
|
530
|
+
const key = JSON.stringify(first ?? null);
|
|
531
|
+
for (let i = 1;i < values.length; i++) {
|
|
532
|
+
if (JSON.stringify(values[i] ?? null) !== key) {
|
|
533
|
+
return { mixed: true, value: undefined };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return { mixed: false, value: first };
|
|
537
|
+
}
|
|
538
|
+
// src/tabs/patch-ops.ts
|
|
539
|
+
var _consumer = null;
|
|
540
|
+
function setPatchConsumer(consumer) {
|
|
541
|
+
_consumer = consumer;
|
|
542
|
+
}
|
|
543
|
+
function getPatchConsumer() {
|
|
544
|
+
return _consumer;
|
|
545
|
+
}
|
|
546
|
+
var _recording = null;
|
|
547
|
+
var _docOps = null;
|
|
548
|
+
var _fmOps = null;
|
|
549
|
+
var _invertible = true;
|
|
550
|
+
function beginRecording() {
|
|
551
|
+
_recording = [];
|
|
552
|
+
_docOps = [];
|
|
553
|
+
_fmOps = [];
|
|
554
|
+
_invertible = true;
|
|
555
|
+
}
|
|
556
|
+
function recordPatch(op) {
|
|
557
|
+
if (_recording) {
|
|
558
|
+
_recording.push(op);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function recordDocOp(pair) {
|
|
562
|
+
if (_docOps) {
|
|
563
|
+
_docOps.push(pair);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function recordFmOp(op) {
|
|
567
|
+
if (_fmOps) {
|
|
568
|
+
_fmOps.push(op);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function endRecording() {
|
|
572
|
+
const record = {
|
|
573
|
+
docOps: _docOps ?? [],
|
|
574
|
+
fmOps: _fmOps ?? [],
|
|
575
|
+
invertible: _invertible,
|
|
576
|
+
ops: _recording ?? []
|
|
577
|
+
};
|
|
578
|
+
_recording = null;
|
|
579
|
+
_docOps = null;
|
|
580
|
+
_fmOps = null;
|
|
581
|
+
_invertible = true;
|
|
582
|
+
return record;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/tabs/transact.ts
|
|
586
|
+
var HISTORY_LIMIT = 100;
|
|
587
|
+
var CHECKPOINT_INTERVAL = 20;
|
|
588
|
+
function patchHistoryEnabled() {
|
|
589
|
+
try {
|
|
590
|
+
return typeof localStorage === "undefined" || !localStorage.getItem("jx-legacy-history");
|
|
591
|
+
} catch {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function setKeyPair(path, key, before, after) {
|
|
596
|
+
return {
|
|
597
|
+
forward: { key, op: "set-key", path, value: after },
|
|
598
|
+
inverse: { key, op: "set-key", path, value: before }
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function shiftPrefixedIndex(parentPath, path, spliceIndex, delta, inclusive) {
|
|
602
|
+
const d = parentPath.length;
|
|
603
|
+
const isProperPrefix = d < path.length && parentPath.every((seg, i) => seg === path[i]);
|
|
604
|
+
if (!isProperPrefix || path[d] !== "children" || typeof path[d + 1] !== "number") {
|
|
605
|
+
return path;
|
|
606
|
+
}
|
|
607
|
+
const idx = path[d + 1];
|
|
608
|
+
if (inclusive ? idx < spliceIndex : idx <= spliceIndex) {
|
|
609
|
+
return path;
|
|
610
|
+
}
|
|
611
|
+
const shifted = [...path];
|
|
612
|
+
shifted[d + 1] = idx + delta;
|
|
613
|
+
return shifted;
|
|
614
|
+
}
|
|
615
|
+
var _transactObserver = null;
|
|
616
|
+
function setTransactObserver(fn) {
|
|
617
|
+
_transactObserver = fn;
|
|
618
|
+
}
|
|
619
|
+
var _transactGate = null;
|
|
620
|
+
function setTransactGate(fn) {
|
|
621
|
+
_transactGate = fn;
|
|
622
|
+
}
|
|
623
|
+
function rollbackFailedTransaction(tab, record, selectionBefore, dirtyBefore) {
|
|
624
|
+
for (let i = record.docOps.length - 1;i >= 0; i--) {
|
|
625
|
+
applyDocOpToDoc(tab.doc.document, toRaw(record.docOps[i].inverse));
|
|
626
|
+
}
|
|
627
|
+
for (let i = record.fmOps.length - 1;i >= 0; i--) {
|
|
628
|
+
const op = record.fmOps[i];
|
|
629
|
+
applyFmState(tab, op.field, op.before);
|
|
630
|
+
}
|
|
631
|
+
tab.doc.document = { ...toRaw(tab.doc.document) };
|
|
632
|
+
tab.session.selection = selectionBefore;
|
|
633
|
+
tab.doc.dirty = dirtyBefore;
|
|
634
|
+
}
|
|
635
|
+
function transactDoc(tab, mutationFn, {
|
|
636
|
+
skipHistory = false,
|
|
637
|
+
origin = "user",
|
|
638
|
+
coalesceKey = null
|
|
639
|
+
} = {}) {
|
|
640
|
+
if (!tab) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
if (origin !== "remote" && _transactGate?.(tab, origin)) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
const selectionBefore = cloneSelection(tab.session.selection);
|
|
647
|
+
const dirtyBefore = tab.doc.dirty;
|
|
648
|
+
beginRecording();
|
|
649
|
+
let record;
|
|
650
|
+
let failure = null;
|
|
651
|
+
try {
|
|
652
|
+
mutationFn(tab);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
failure = { error };
|
|
655
|
+
} finally {
|
|
656
|
+
record = endRecording();
|
|
657
|
+
}
|
|
658
|
+
if (failure) {
|
|
659
|
+
rollbackFailedTransaction(tab, record, selectionBefore, dirtyBefore);
|
|
660
|
+
throw failure.error;
|
|
661
|
+
}
|
|
662
|
+
const raw = toRaw(tab.doc.document);
|
|
663
|
+
const newRef = { ...raw };
|
|
664
|
+
const consumer = getPatchConsumer();
|
|
665
|
+
const verdict = consumer === null ? { patchable: false, reason: "no-patch-consumer" } : record.ops.length === 0 ? { patchable: false, reason: "unrecorded-mutation" } : consumer.classify(tab, record.ops);
|
|
666
|
+
if (verdict.patchable) {
|
|
667
|
+
consumer.markConsumed(newRef, tab);
|
|
668
|
+
}
|
|
669
|
+
tab.doc.document = newRef;
|
|
670
|
+
if (verdict.patchable) {
|
|
671
|
+
try {
|
|
672
|
+
consumer.apply(tab, record.ops, record);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
consumer.escalate(`patch-apply-failed: ${error instanceof Error ? error.message : String(error)}`, tab);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!skipHistory && !_batchTab) {
|
|
678
|
+
pushHistoryEntry(tab, raw, record, selectionBefore, coalesceKey);
|
|
679
|
+
}
|
|
680
|
+
tab.doc.dirty = true;
|
|
681
|
+
_transactObserver?.(tab, record, origin);
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
function pushHistoryEntry(tab, raw, record, selectionBefore, coalesceKey = null) {
|
|
685
|
+
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
686
|
+
const useOps = patchHistoryEnabled() && record.invertible && (record.docOps.length > 0 || record.fmOps.length > 0);
|
|
687
|
+
const prev = truncated.at(-1);
|
|
688
|
+
if (coalesceKey && useOps && prev && prev.coalesceKey === coalesceKey && prev.inverseOps) {
|
|
689
|
+
prev.forwardOps = [...prev.forwardOps ?? [], ...record.docOps.map((pair) => pair.forward)];
|
|
690
|
+
prev.inverseOps = [...prev.inverseOps, ...record.docOps.map((pair) => pair.inverse)];
|
|
691
|
+
prev.selection = cloneSelection(tab.session.selection);
|
|
692
|
+
if (prev.document) {
|
|
693
|
+
prev.document = jsonClone(raw);
|
|
694
|
+
}
|
|
695
|
+
tab.history.snapshots = truncated;
|
|
696
|
+
tab.history.index = truncated.length - 1;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const needCheckpoint = !useOps || truncated.length % CHECKPOINT_INTERVAL === 0;
|
|
700
|
+
truncated.push({
|
|
701
|
+
document: needCheckpoint ? jsonClone(raw) : null,
|
|
702
|
+
fmOps: useOps && record.fmOps.length > 0 ? record.fmOps : null,
|
|
703
|
+
forwardOps: useOps ? record.docOps.map((p) => p.forward) : null,
|
|
704
|
+
inverseOps: useOps ? record.docOps.map((p) => p.inverse) : null,
|
|
705
|
+
coalesceKey,
|
|
706
|
+
selection: cloneSelection(tab.session.selection),
|
|
707
|
+
selectionBefore
|
|
708
|
+
});
|
|
709
|
+
if (truncated.length > HISTORY_LIMIT) {
|
|
710
|
+
if (!truncated[1].document) {
|
|
711
|
+
truncated[1].document = materializeState(truncated, 1);
|
|
712
|
+
}
|
|
713
|
+
truncated.shift();
|
|
714
|
+
}
|
|
715
|
+
tab.history.snapshots = truncated;
|
|
716
|
+
tab.history.index = truncated.length - 1;
|
|
717
|
+
}
|
|
718
|
+
function materializeState(snapshots, target) {
|
|
719
|
+
let base = target;
|
|
720
|
+
while (base >= 0 && !snapshots[base].document) {
|
|
721
|
+
base -= 1;
|
|
722
|
+
}
|
|
723
|
+
if (base < 0) {
|
|
724
|
+
throw new Error("history-missing-checkpoint");
|
|
725
|
+
}
|
|
726
|
+
const doc = jsonClone(snapshots[base].document);
|
|
727
|
+
for (let i = base + 1;i <= target; i++) {
|
|
728
|
+
for (const op of snapshots[i].forwardOps ?? []) {
|
|
729
|
+
applyDocOpToDoc(doc, op);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return doc;
|
|
733
|
+
}
|
|
734
|
+
function transact(tab, fn, opts) {
|
|
735
|
+
return transactDoc(tab, (t) => fn(t.doc.document), opts);
|
|
736
|
+
}
|
|
737
|
+
function applyExternalDocOps(tab, ops) {
|
|
738
|
+
transactDoc(tab, (t) => {
|
|
739
|
+
for (const op of ops) {
|
|
740
|
+
applyDocOp(t, op);
|
|
741
|
+
}
|
|
742
|
+
}, { origin: "remote", skipHistory: true });
|
|
743
|
+
}
|
|
744
|
+
var DOC_META_KEYS = new Set(["state", "$media", "$head", "$elements", "imports", "$layout"]);
|
|
745
|
+
function applyDocOp(tab, op) {
|
|
746
|
+
const prev = op.op === "set-key" ? getNodeAtPath(tab.doc.document, op.path)?.[op.key] : null;
|
|
747
|
+
applyDocOpToDoc(tab.doc.document, op);
|
|
748
|
+
recordDocOp({ forward: op, inverse: op });
|
|
749
|
+
switch (op.op) {
|
|
750
|
+
case "set-key": {
|
|
751
|
+
if (DOC_META_KEYS.has(op.key)) {
|
|
752
|
+
recordPatch({ key: op.key, op: "doc-meta" });
|
|
753
|
+
} else if (op.key === "style") {
|
|
754
|
+
recordPatch({ op: "set-style", path: op.path });
|
|
755
|
+
} else if (op.key === "textContent") {
|
|
756
|
+
recordPatch({ op: "set-text", path: op.path });
|
|
757
|
+
} else if (op.key === "attributes" || op.key === "cases" || op.key === "$props") {
|
|
758
|
+
recordPatch({ op: "replace", path: op.path });
|
|
759
|
+
} else {
|
|
760
|
+
const isEvent = op.key.startsWith("on") && (isEventBinding(op.value) || isEventBinding(prev));
|
|
761
|
+
recordPatch({ isEvent, key: op.key, op: "set-prop", path: op.path });
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
case "insert-child": {
|
|
766
|
+
recordPatch({ index: op.index, op: "insert", parentPath: op.parentPath });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
case "remove-child": {
|
|
770
|
+
recordPatch({ op: "remove", path: [...op.parentPath, "children", op.index] });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
case "set-child": {
|
|
774
|
+
recordPatch({ op: "replace", path: [...op.parentPath, "children", op.index] });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
case "move-child": {
|
|
778
|
+
recordPatch({
|
|
779
|
+
fromPath: [...op.fromParentPath, "children", op.fromIndex],
|
|
780
|
+
op: "move",
|
|
781
|
+
toIndex: op.toIndex,
|
|
782
|
+
toParentPath: op.toParentPath
|
|
783
|
+
});
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
default: {
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
var _batchTab = null;
|
|
792
|
+
var _batchEndNotifier = null;
|
|
793
|
+
function setBatchEndNotifier(fn) {
|
|
794
|
+
_batchEndNotifier = fn;
|
|
795
|
+
}
|
|
796
|
+
function beginBatch(tab) {
|
|
797
|
+
_batchTab = tab;
|
|
798
|
+
}
|
|
799
|
+
function endBatch() {
|
|
800
|
+
const tab = _batchTab;
|
|
801
|
+
if (tab && !_historyDelegates.has(tab)) {
|
|
802
|
+
const raw = toRaw(tab.doc.document);
|
|
803
|
+
const snapshot = {
|
|
804
|
+
document: jsonClone(raw),
|
|
805
|
+
selection: cloneSelection(tab.session.selection)
|
|
806
|
+
};
|
|
807
|
+
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
808
|
+
truncated.push(snapshot);
|
|
809
|
+
if (truncated.length > HISTORY_LIMIT) {
|
|
810
|
+
truncated.shift();
|
|
811
|
+
}
|
|
812
|
+
tab.history.snapshots = truncated;
|
|
813
|
+
tab.history.index = truncated.length - 1;
|
|
814
|
+
}
|
|
815
|
+
_batchTab = null;
|
|
816
|
+
if (tab) {
|
|
817
|
+
_batchEndNotifier?.(tab);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function isBatching() {
|
|
821
|
+
return _batchTab !== null;
|
|
822
|
+
}
|
|
823
|
+
function batchTab() {
|
|
824
|
+
return _batchTab;
|
|
825
|
+
}
|
|
826
|
+
var _historyDelegates = new WeakMap;
|
|
827
|
+
function setHistoryDelegate(tab, delegate) {
|
|
828
|
+
if (delegate) {
|
|
829
|
+
_historyDelegates.set(tab, delegate);
|
|
830
|
+
} else {
|
|
831
|
+
_historyDelegates.delete(tab);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function canUndo(tab) {
|
|
835
|
+
const delegate = _historyDelegates.get(tab);
|
|
836
|
+
return delegate ? delegate.canUndo(tab) : tab.history.index > 0;
|
|
837
|
+
}
|
|
838
|
+
function canRedo(tab) {
|
|
839
|
+
const delegate = _historyDelegates.get(tab);
|
|
840
|
+
return delegate ? delegate.canRedo(tab) : tab.history.index < tab.history.snapshots.length - 1;
|
|
841
|
+
}
|
|
842
|
+
function restoreState(tab, index) {
|
|
843
|
+
const { snapshots } = tab.history;
|
|
844
|
+
tab.doc.document = materializeState(snapshots, index);
|
|
845
|
+
const snap = snapshots[index];
|
|
846
|
+
tab.session.selection = cloneSelection(toRaw(snap.selection));
|
|
847
|
+
}
|
|
848
|
+
function assertHistoryConsistency(tab, index) {
|
|
849
|
+
try {
|
|
850
|
+
if (typeof localStorage === "undefined" || !localStorage.getItem("jx-canvas-debug")) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const expected = JSON.stringify(materializeState(tab.history.snapshots, index));
|
|
857
|
+
const actual = JSON.stringify(jsonClone(toRaw(tab.doc.document)));
|
|
858
|
+
if (expected !== actual) {
|
|
859
|
+
console.error("jx history: ops-based undo/redo diverged from checkpoint replay", {
|
|
860
|
+
index
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function undo(tab) {
|
|
865
|
+
const delegate = _historyDelegates.get(tab);
|
|
866
|
+
if (delegate) {
|
|
867
|
+
delegate.undo(tab);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (tab.history.index <= 0) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const entry = tab.history.snapshots[tab.history.index];
|
|
874
|
+
const inverseOps = patchHistoryEnabled() ? entry.inverseOps : null;
|
|
875
|
+
const fmOps = patchHistoryEnabled() ? entry.fmOps : null;
|
|
876
|
+
if (inverseOps && inverseOps.length > 0 || fmOps && fmOps.length > 0) {
|
|
877
|
+
const applied = transactDoc(tab, (t) => {
|
|
878
|
+
if (inverseOps) {
|
|
879
|
+
for (let i = inverseOps.length - 1;i >= 0; i--) {
|
|
880
|
+
applyDocOp(t, toRaw(inverseOps[i]));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (fmOps) {
|
|
884
|
+
for (let i = fmOps.length - 1;i >= 0; i--) {
|
|
885
|
+
const op = toRaw(fmOps[i]);
|
|
886
|
+
applyFmState(t, op.field, op.before);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}, { origin: "history", skipHistory: true });
|
|
890
|
+
if (!applied) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
tab.session.selection = cloneSelection(toRaw(entry.selectionBefore ?? []));
|
|
894
|
+
tab.history.index -= 1;
|
|
895
|
+
assertHistoryConsistency(tab, tab.history.index);
|
|
896
|
+
} else {
|
|
897
|
+
tab.history.index -= 1;
|
|
898
|
+
restoreState(tab, tab.history.index);
|
|
899
|
+
}
|
|
900
|
+
tab.doc.dirty = true;
|
|
901
|
+
}
|
|
902
|
+
function redo(tab) {
|
|
903
|
+
const delegate = _historyDelegates.get(tab);
|
|
904
|
+
if (delegate) {
|
|
905
|
+
delegate.redo(tab);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (tab.history.index >= tab.history.snapshots.length - 1) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const entry = tab.history.snapshots[tab.history.index + 1];
|
|
912
|
+
const forwardOps = patchHistoryEnabled() ? entry.forwardOps : null;
|
|
913
|
+
const fmOps = patchHistoryEnabled() ? entry.fmOps : null;
|
|
914
|
+
if (forwardOps && forwardOps.length > 0 || fmOps && fmOps.length > 0) {
|
|
915
|
+
const applied = transactDoc(tab, (t) => {
|
|
916
|
+
if (forwardOps) {
|
|
917
|
+
for (const op of forwardOps) {
|
|
918
|
+
applyDocOp(t, toRaw(op));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (fmOps) {
|
|
922
|
+
for (const op of fmOps) {
|
|
923
|
+
const raw = toRaw(op);
|
|
924
|
+
applyFmState(t, raw.field, raw.after);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}, { origin: "history", skipHistory: true });
|
|
928
|
+
if (!applied) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
tab.session.selection = cloneSelection(toRaw(entry.selection));
|
|
932
|
+
tab.history.index += 1;
|
|
933
|
+
assertHistoryConsistency(tab, tab.history.index);
|
|
934
|
+
} else {
|
|
935
|
+
tab.history.index += 1;
|
|
936
|
+
restoreState(tab, tab.history.index);
|
|
937
|
+
}
|
|
938
|
+
tab.doc.dirty = true;
|
|
939
|
+
}
|
|
940
|
+
function mutateInsertNode(tab, parentPath, index, nodeDef) {
|
|
941
|
+
const parent = getNodeAtPath(tab.doc.document, parentPath);
|
|
942
|
+
if (!parent) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
childArray(parent).splice(index, 0, structuredClone(nodeDef));
|
|
946
|
+
recordPatch({ index, op: "insert", parentPath });
|
|
947
|
+
recordDocOp({
|
|
948
|
+
forward: { index, node: cloneValue(nodeDef), op: "insert-child", parentPath },
|
|
949
|
+
inverse: { index, op: "remove-child", parentPath }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
function mutateRemoveNode(tab, path) {
|
|
953
|
+
if (!path || !isSpliceablePath(path)) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const elemPath = parentElementPath(path);
|
|
957
|
+
const idx = childIndex(path);
|
|
958
|
+
const [removed] = getNodeAtPath(tab.doc.document, elemPath).children.splice(idx, 1);
|
|
959
|
+
recordPatch({ op: "remove", path });
|
|
960
|
+
recordDocOp({
|
|
961
|
+
forward: { index: idx, op: "remove-child", parentPath: elemPath },
|
|
962
|
+
inverse: { index: idx, node: cloneValue(removed), op: "insert-child", parentPath: elemPath }
|
|
963
|
+
});
|
|
964
|
+
tab.session.selection = pruneSelection(tab.session.selection, path);
|
|
965
|
+
}
|
|
966
|
+
function mutateDuplicateNode(tab, path) {
|
|
967
|
+
if (!path || !isSpliceablePath(path)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
971
|
+
if (!node) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const elemPath = parentElementPath(path);
|
|
975
|
+
const idx = childIndex(path);
|
|
976
|
+
const clone = structuredClone(toRaw(node));
|
|
977
|
+
getNodeAtPath(tab.doc.document, elemPath).children.splice(idx + 1, 0, clone);
|
|
978
|
+
recordPatch({ index: idx + 1, op: "insert", parentPath: elemPath });
|
|
979
|
+
recordDocOp({
|
|
980
|
+
forward: { index: idx + 1, node: cloneValue(clone), op: "insert-child", parentPath: elemPath },
|
|
981
|
+
inverse: { index: idx + 1, op: "remove-child", parentPath: elemPath }
|
|
982
|
+
});
|
|
983
|
+
tab.session.selection = [[...elemPath, "children", idx + 1]];
|
|
984
|
+
}
|
|
985
|
+
function mutateRemoveNodes(tab, paths) {
|
|
986
|
+
for (const path of structuralBatch(paths)) {
|
|
987
|
+
mutateRemoveNode(tab, path);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function mutateDuplicateNodes(tab, paths) {
|
|
991
|
+
let clones = [];
|
|
992
|
+
for (const path of structuralBatch(paths)) {
|
|
993
|
+
const before = tab.session.selection;
|
|
994
|
+
mutateDuplicateNode(tab, path);
|
|
995
|
+
const made = primarySelection(tab.session.selection);
|
|
996
|
+
if (!made || tab.session.selection === before) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const parentPath = parentElementPath(made);
|
|
1000
|
+
const spliceIndex = childIndex(made);
|
|
1001
|
+
clones = clones.map((clone) => shiftPrefixedIndex(parentPath, clone, spliceIndex, 1, true));
|
|
1002
|
+
clones.unshift(made);
|
|
1003
|
+
}
|
|
1004
|
+
if (clones.length > 0) {
|
|
1005
|
+
tab.session.selection = clones;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function mutateWrapNode(tab, path, wrapperTag = "div") {
|
|
1009
|
+
if (!path || !isSpliceablePath(path)) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1013
|
+
if (!node) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const elemPath = parentElementPath(path);
|
|
1017
|
+
const idx = childIndex(path);
|
|
1018
|
+
const wrapper = {
|
|
1019
|
+
children: [structuredClone(toRaw(node))],
|
|
1020
|
+
tagName: wrapperTag
|
|
1021
|
+
};
|
|
1022
|
+
const original = cloneValue(node);
|
|
1023
|
+
getNodeAtPath(tab.doc.document, elemPath).children.splice(idx, 1, wrapper);
|
|
1024
|
+
recordPatch({ op: "replace", path });
|
|
1025
|
+
recordDocOp({
|
|
1026
|
+
forward: { index: idx, node: cloneValue(wrapper), op: "set-child", parentPath: elemPath },
|
|
1027
|
+
inverse: { index: idx, node: original, op: "set-child", parentPath: elemPath }
|
|
1028
|
+
});
|
|
1029
|
+
tab.session.selection = [[...elemPath, "children", idx]];
|
|
1030
|
+
}
|
|
1031
|
+
function mutateMoveNode(tab, fromPath, toParentPath, toIndex) {
|
|
1032
|
+
const doc = tab.doc.document;
|
|
1033
|
+
const fromParentPath = parentElementPath(fromPath);
|
|
1034
|
+
const fromIdx = childIndex(fromPath);
|
|
1035
|
+
if (!fromParentPath || typeof fromIdx !== "number") {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const fromParent = getNodeAtPath(doc, fromParentPath);
|
|
1039
|
+
const toParent = getNodeAtPath(doc, toParentPath);
|
|
1040
|
+
if (!fromParent || !Array.isArray(fromParent.children) || !toParent) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const [node] = fromParent.children.splice(fromIdx, 1);
|
|
1044
|
+
if (node === undefined) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
let adjustedIndex = toIndex;
|
|
1048
|
+
if (fromParent === toParent && fromIdx < toIndex) {
|
|
1049
|
+
adjustedIndex -= 1;
|
|
1050
|
+
}
|
|
1051
|
+
childArray(toParent).splice(adjustedIndex, 0, node);
|
|
1052
|
+
recordPatch({ fromPath, op: "move", toIndex: adjustedIndex, toParentPath });
|
|
1053
|
+
recordDocOp({
|
|
1054
|
+
forward: {
|
|
1055
|
+
fromIndex: fromIdx,
|
|
1056
|
+
fromParentPath,
|
|
1057
|
+
op: "move-child",
|
|
1058
|
+
toIndex: adjustedIndex,
|
|
1059
|
+
toParentPath
|
|
1060
|
+
},
|
|
1061
|
+
inverse: {
|
|
1062
|
+
fromIndex: adjustedIndex,
|
|
1063
|
+
fromParentPath: shiftPrefixedIndex(fromParentPath, toParentPath, fromIdx, -1, false),
|
|
1064
|
+
op: "move-child",
|
|
1065
|
+
toIndex: fromIdx,
|
|
1066
|
+
toParentPath: shiftPrefixedIndex(toParentPath, fromParentPath, adjustedIndex, 1, true)
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
if (pathsEqual(primarySelection(tab.session.selection), fromPath)) {
|
|
1070
|
+
let idx = toIndex;
|
|
1071
|
+
if (fromParentPath.length === toParentPath.length && fromParentPath.every((v, i) => v === toParentPath[i]) && fromIdx < toIndex) {
|
|
1072
|
+
idx = toIndex - 1;
|
|
1073
|
+
}
|
|
1074
|
+
tab.session.selection = [[...toParentPath, "children", idx]];
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function mutateUpdateProperty(tab, path, key, value) {
|
|
1078
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1079
|
+
if (!node) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const prev = node[key];
|
|
1083
|
+
const before = cloneValue(prev);
|
|
1084
|
+
if (value === undefined || value === null || value === "") {
|
|
1085
|
+
delete node[key];
|
|
1086
|
+
} else {
|
|
1087
|
+
node[key] = value;
|
|
1088
|
+
}
|
|
1089
|
+
recordDocOp(setKeyPair(path, key, before, cloneValue(node[key])));
|
|
1090
|
+
if (key === "textContent") {
|
|
1091
|
+
recordPatch({ op: "set-text", path });
|
|
1092
|
+
} else if (key === "style") {
|
|
1093
|
+
recordPatch({ op: "set-style", path });
|
|
1094
|
+
} else {
|
|
1095
|
+
const bindingValue = value === undefined || value === null || value === "" ? prev : value;
|
|
1096
|
+
const isEvent = key.startsWith("on") && isEventBinding(bindingValue);
|
|
1097
|
+
recordPatch({ isEvent, key, op: "set-prop", path });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function mutateUpdateStyle(tab, path, prop, value) {
|
|
1101
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1102
|
+
const styleBefore = cloneValue(node.style);
|
|
1103
|
+
if (!node.style) {
|
|
1104
|
+
node.style = {};
|
|
1105
|
+
}
|
|
1106
|
+
if (value === undefined || value === "") {
|
|
1107
|
+
delete node.style[prop];
|
|
1108
|
+
} else {
|
|
1109
|
+
node.style[prop] = value;
|
|
1110
|
+
}
|
|
1111
|
+
if (Object.keys(node.style).length === 0) {
|
|
1112
|
+
delete node.style;
|
|
1113
|
+
}
|
|
1114
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1115
|
+
recordPatch({ op: "set-style", path });
|
|
1116
|
+
}
|
|
1117
|
+
function mutateUpdateAttribute(tab, path, attr, value) {
|
|
1118
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1119
|
+
const attrsBefore = cloneValue(node.attributes);
|
|
1120
|
+
if (!node.attributes) {
|
|
1121
|
+
node.attributes = {};
|
|
1122
|
+
}
|
|
1123
|
+
if (value === undefined || value === "") {
|
|
1124
|
+
delete node.attributes[attr];
|
|
1125
|
+
} else {
|
|
1126
|
+
node.attributes[attr] = value;
|
|
1127
|
+
}
|
|
1128
|
+
if (Object.keys(node.attributes).length === 0) {
|
|
1129
|
+
delete node.attributes;
|
|
1130
|
+
}
|
|
1131
|
+
recordDocOp(setKeyPair(path, "attributes", attrsBefore, cloneValue(node.attributes)));
|
|
1132
|
+
recordPatch({ attr, op: "set-attr", path });
|
|
1133
|
+
}
|
|
1134
|
+
function mutateUpdateMediaStyle(tab, path, mediaName, prop, value) {
|
|
1135
|
+
if (!mediaName) {
|
|
1136
|
+
return mutateUpdateStyle(tab, path, prop, value);
|
|
1137
|
+
}
|
|
1138
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1139
|
+
const styleBefore = cloneValue(node.style);
|
|
1140
|
+
if (!node.style) {
|
|
1141
|
+
node.style = {};
|
|
1142
|
+
}
|
|
1143
|
+
const key = `@${mediaName}`;
|
|
1144
|
+
const media = ensureNestedStyle(node.style, key);
|
|
1145
|
+
if (value === undefined || value === "") {
|
|
1146
|
+
delete media[prop];
|
|
1147
|
+
if (Object.keys(media).length === 0) {
|
|
1148
|
+
delete node.style[key];
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
media[prop] = value;
|
|
1152
|
+
}
|
|
1153
|
+
if (Object.keys(node.style).length === 0) {
|
|
1154
|
+
delete node.style;
|
|
1155
|
+
}
|
|
1156
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1157
|
+
recordPatch({ op: "set-style", path });
|
|
1158
|
+
}
|
|
1159
|
+
function mutateUpdateNestedStyle(tab, path, selector, prop, value) {
|
|
1160
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1161
|
+
const styleBefore = cloneValue(node.style);
|
|
1162
|
+
if (!node.style) {
|
|
1163
|
+
node.style = {};
|
|
1164
|
+
}
|
|
1165
|
+
const block = ensureNestedStyle(node.style, selector);
|
|
1166
|
+
if (value === undefined || value === "") {
|
|
1167
|
+
delete block[prop];
|
|
1168
|
+
if (Object.keys(block).length === 0) {
|
|
1169
|
+
delete node.style[selector];
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
block[prop] = value;
|
|
1173
|
+
}
|
|
1174
|
+
if (Object.keys(node.style).length === 0) {
|
|
1175
|
+
delete node.style;
|
|
1176
|
+
}
|
|
1177
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1178
|
+
recordPatch({ op: "set-style", path });
|
|
1179
|
+
}
|
|
1180
|
+
function mutateUpdateMediaNestedStyle(tab, path, mediaName, selector, prop, value) {
|
|
1181
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1182
|
+
const styleBefore = cloneValue(node.style);
|
|
1183
|
+
if (!node.style) {
|
|
1184
|
+
node.style = {};
|
|
1185
|
+
}
|
|
1186
|
+
const key = `@${mediaName}`;
|
|
1187
|
+
const media = ensureNestedStyle(node.style, key);
|
|
1188
|
+
const block = ensureNestedStyle(media, selector);
|
|
1189
|
+
if (value === undefined || value === "") {
|
|
1190
|
+
delete block[prop];
|
|
1191
|
+
if (Object.keys(block).length === 0) {
|
|
1192
|
+
delete media[selector];
|
|
1193
|
+
}
|
|
1194
|
+
if (Object.keys(media).length === 0) {
|
|
1195
|
+
delete node.style[key];
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
block[prop] = value;
|
|
1199
|
+
}
|
|
1200
|
+
if (Object.keys(node.style).length === 0) {
|
|
1201
|
+
delete node.style;
|
|
1202
|
+
}
|
|
1203
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1204
|
+
recordPatch({ op: "set-style", path });
|
|
1205
|
+
}
|
|
1206
|
+
function pruneEmptyStylePath(root, stylePath) {
|
|
1207
|
+
const owners = [root];
|
|
1208
|
+
for (const seg of stylePath) {
|
|
1209
|
+
const next = getNestedStyle(owners.at(-1), seg);
|
|
1210
|
+
if (!next) {
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
owners.push(next);
|
|
1214
|
+
}
|
|
1215
|
+
for (let i = owners.length - 2;i >= 0; i--) {
|
|
1216
|
+
const owner = owners[i];
|
|
1217
|
+
const seg = stylePath[i];
|
|
1218
|
+
const block = getNestedStyle(owner, seg);
|
|
1219
|
+
if (block && Object.keys(block).length === 0) {
|
|
1220
|
+
delete owner[seg];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function mutateUpdateNestedStylePath(tab, path, stylePath, prop, value) {
|
|
1225
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1226
|
+
const styleBefore = cloneValue(node.style);
|
|
1227
|
+
if (!node.style) {
|
|
1228
|
+
node.style = {};
|
|
1229
|
+
}
|
|
1230
|
+
let obj = node.style;
|
|
1231
|
+
for (const seg of stylePath) {
|
|
1232
|
+
obj = ensureNestedStyle(obj, seg);
|
|
1233
|
+
}
|
|
1234
|
+
if (value === undefined || value === "") {
|
|
1235
|
+
delete obj[prop];
|
|
1236
|
+
pruneEmptyStylePath(node.style, stylePath);
|
|
1237
|
+
} else {
|
|
1238
|
+
obj[prop] = value;
|
|
1239
|
+
}
|
|
1240
|
+
if (Object.keys(node.style).length === 0) {
|
|
1241
|
+
delete node.style;
|
|
1242
|
+
}
|
|
1243
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1244
|
+
recordPatch({ op: "set-style", path });
|
|
1245
|
+
}
|
|
1246
|
+
function mutateUpdateMediaNestedStylePath(tab, path, mediaName, stylePath, prop, value) {
|
|
1247
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1248
|
+
const styleBefore = cloneValue(node.style);
|
|
1249
|
+
if (!node.style) {
|
|
1250
|
+
node.style = {};
|
|
1251
|
+
}
|
|
1252
|
+
const key = `@${mediaName}`;
|
|
1253
|
+
const media = ensureNestedStyle(node.style, key);
|
|
1254
|
+
let obj = media;
|
|
1255
|
+
for (const seg of stylePath) {
|
|
1256
|
+
obj = ensureNestedStyle(obj, seg);
|
|
1257
|
+
}
|
|
1258
|
+
if (value === undefined || value === "") {
|
|
1259
|
+
delete obj[prop];
|
|
1260
|
+
pruneEmptyStylePath(media, stylePath);
|
|
1261
|
+
if (Object.keys(media).length === 0) {
|
|
1262
|
+
delete node.style[key];
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
obj[prop] = value;
|
|
1266
|
+
}
|
|
1267
|
+
if (Object.keys(node.style).length === 0) {
|
|
1268
|
+
delete node.style;
|
|
1269
|
+
}
|
|
1270
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1271
|
+
recordPatch({ op: "set-style", path });
|
|
1272
|
+
}
|
|
1273
|
+
function mutateReplaceStyle(tab, path, style) {
|
|
1274
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1275
|
+
const styleBefore = cloneValue(node.style);
|
|
1276
|
+
if (style && Object.keys(style).length > 0) {
|
|
1277
|
+
node.style = style;
|
|
1278
|
+
} else {
|
|
1279
|
+
delete node.style;
|
|
1280
|
+
}
|
|
1281
|
+
recordDocOp(setKeyPair(path, "style", styleBefore, cloneValue(node.style)));
|
|
1282
|
+
recordPatch({ op: "set-style", path });
|
|
1283
|
+
}
|
|
1284
|
+
function mutateAddDef(tab, name, def) {
|
|
1285
|
+
const doc = tab.doc.document;
|
|
1286
|
+
const before = cloneValue(doc.state);
|
|
1287
|
+
if (!doc.state) {
|
|
1288
|
+
doc.state = {};
|
|
1289
|
+
}
|
|
1290
|
+
doc.state[name] = def;
|
|
1291
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
1292
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
1293
|
+
}
|
|
1294
|
+
function mutateRemoveDef(tab, name) {
|
|
1295
|
+
const doc = tab.doc.document;
|
|
1296
|
+
if (doc.state) {
|
|
1297
|
+
const before = cloneValue(doc.state);
|
|
1298
|
+
delete doc.state[name];
|
|
1299
|
+
if (Object.keys(doc.state).length === 0) {
|
|
1300
|
+
delete doc.state;
|
|
1301
|
+
}
|
|
1302
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
1303
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function mutateUpdateDef(tab, name, updates) {
|
|
1307
|
+
const doc = tab.doc.document;
|
|
1308
|
+
const stateBefore = cloneValue(doc.state);
|
|
1309
|
+
if (!doc.state) {
|
|
1310
|
+
doc.state = {};
|
|
1311
|
+
}
|
|
1312
|
+
const existing = doc.state[name];
|
|
1313
|
+
const entry = existing == null ? {} : typeof existing !== "object" || Array.isArray(existing) ? { default: existing } : existing;
|
|
1314
|
+
doc.state[name] = entry;
|
|
1315
|
+
Object.assign(entry, updates);
|
|
1316
|
+
for (const k of Object.keys(entry)) {
|
|
1317
|
+
if (entry[k] === undefined || entry[k] === null) {
|
|
1318
|
+
delete entry[k];
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
recordDocOp(setKeyPair([], "state", stateBefore, cloneValue(doc.state)));
|
|
1322
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
1323
|
+
}
|
|
1324
|
+
function mutateRenameDef(tab, oldName, newName) {
|
|
1325
|
+
const doc = tab.doc.document;
|
|
1326
|
+
if (!doc.state || !doc.state[oldName]) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const before = cloneValue(doc.state);
|
|
1330
|
+
doc.state[newName] = doc.state[oldName];
|
|
1331
|
+
delete doc.state[oldName];
|
|
1332
|
+
recordDocOp(setKeyPair([], "state", before, cloneValue(doc.state)));
|
|
1333
|
+
recordPatch({ key: "state", op: "doc-meta" });
|
|
1334
|
+
}
|
|
1335
|
+
function mutateUpdateProp(tab, path, propName, value) {
|
|
1336
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1337
|
+
const propsBefore = cloneValue(node.$props);
|
|
1338
|
+
if (!node.$props) {
|
|
1339
|
+
node.$props = {};
|
|
1340
|
+
}
|
|
1341
|
+
if (value === undefined || value === null || value === "") {
|
|
1342
|
+
delete node.$props[propName];
|
|
1343
|
+
} else {
|
|
1344
|
+
node.$props[propName] = value;
|
|
1345
|
+
}
|
|
1346
|
+
if (Object.keys(node.$props).length === 0) {
|
|
1347
|
+
delete node.$props;
|
|
1348
|
+
}
|
|
1349
|
+
recordDocOp(setKeyPair(path, "$props", propsBefore, cloneValue(node.$props)));
|
|
1350
|
+
recordPatch({ op: "replace", path });
|
|
1351
|
+
}
|
|
1352
|
+
function mutateAddSwitchCase(tab, path, caseName, caseDef) {
|
|
1353
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1354
|
+
const before = cloneValue(node.cases);
|
|
1355
|
+
if (!node.cases) {
|
|
1356
|
+
node.cases = {};
|
|
1357
|
+
}
|
|
1358
|
+
node.cases[caseName] = caseDef || { tagName: "div", textContent: caseName };
|
|
1359
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1360
|
+
recordPatch({ op: "replace", path });
|
|
1361
|
+
}
|
|
1362
|
+
function mutateRemoveSwitchCase(tab, path, caseName) {
|
|
1363
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1364
|
+
if (node.cases) {
|
|
1365
|
+
const before = cloneValue(node.cases);
|
|
1366
|
+
delete node.cases[caseName];
|
|
1367
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1368
|
+
recordPatch({ op: "replace", path });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function mutateRenameSwitchCase(tab, path, oldName, newName) {
|
|
1372
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
1373
|
+
if (!node.cases || !node.cases[oldName]) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const before = cloneValue(node.cases);
|
|
1377
|
+
node.cases[newName] = node.cases[oldName];
|
|
1378
|
+
delete node.cases[oldName];
|
|
1379
|
+
recordDocOp(setKeyPair(path, "cases", before, cloneValue(node.cases)));
|
|
1380
|
+
recordPatch({ op: "replace", path });
|
|
1381
|
+
}
|
|
1382
|
+
function mutateUpdateFrontmatter(tab, field, value) {
|
|
1383
|
+
const fm = tab.doc.content.frontmatter;
|
|
1384
|
+
const before = Object.hasOwn(fm, field) ? cloneValue(fm[field]) : undefined;
|
|
1385
|
+
const deletes = value === undefined || value === null || value === "";
|
|
1386
|
+
if (deletes) {
|
|
1387
|
+
delete fm[field];
|
|
1388
|
+
} else {
|
|
1389
|
+
fm[field] = value;
|
|
1390
|
+
}
|
|
1391
|
+
const after = deletes ? undefined : cloneValue(value);
|
|
1392
|
+
recordFmOp({ after, before, field });
|
|
1393
|
+
tab.doc.dirty = true;
|
|
1394
|
+
}
|
|
1395
|
+
function applyFmState(tab, field, value) {
|
|
1396
|
+
if (value === undefined) {
|
|
1397
|
+
delete tab.doc.content.frontmatter[field];
|
|
1398
|
+
} else {
|
|
1399
|
+
tab.doc.content.frontmatter[field] = value;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/commands/context.ts
|
|
1404
|
+
var EDITOR_KIND_BY_MODE = {
|
|
1405
|
+
design: "canvas",
|
|
1406
|
+
edit: "canvas",
|
|
1407
|
+
entry: "entry",
|
|
1408
|
+
"git-diff": "diff",
|
|
1409
|
+
grid: "grid",
|
|
1410
|
+
manage: "library",
|
|
1411
|
+
media: "media",
|
|
1412
|
+
preview: "canvas",
|
|
1413
|
+
settings: "config",
|
|
1414
|
+
source: "code",
|
|
1415
|
+
stylebook: "config"
|
|
1416
|
+
};
|
|
1417
|
+
var EDITOR_KIND_LABELS = {
|
|
1418
|
+
canvas: "Canvas",
|
|
1419
|
+
code: "Code",
|
|
1420
|
+
config: "Project Styles",
|
|
1421
|
+
diff: "Diff",
|
|
1422
|
+
entry: "Entry",
|
|
1423
|
+
grid: "Grid",
|
|
1424
|
+
media: "Media",
|
|
1425
|
+
library: "Library",
|
|
1426
|
+
none: "None"
|
|
1427
|
+
};
|
|
1428
|
+
function editorKindForMode(mode) {
|
|
1429
|
+
return EDITOR_KIND_BY_MODE[mode] ?? "canvas";
|
|
1430
|
+
}
|
|
1431
|
+
var CANVAS_VIEW_BY_MODE = {
|
|
1432
|
+
design: "design",
|
|
1433
|
+
edit: "edit",
|
|
1434
|
+
preview: "preview"
|
|
1435
|
+
};
|
|
1436
|
+
function canvasViewForMode(mode) {
|
|
1437
|
+
return CANVAS_VIEW_BY_MODE[mode] ?? "design";
|
|
1438
|
+
}
|
|
1439
|
+
var inCanvas = (ctx) => ctx.editor.kind === "canvas";
|
|
1440
|
+
var hasSelection = (ctx) => inCanvas(ctx) && ctx.selection.count > 0;
|
|
1441
|
+
var hasElementSelection = (ctx) => hasSelection(ctx) && !ctx.selection.isRoot;
|
|
1442
|
+
function emptyContext() {
|
|
1443
|
+
return {
|
|
1444
|
+
project: { open: false, isSite: false, isRepo: false, isMultilingual: false },
|
|
1445
|
+
git: { ahead: 0, behind: 0, dirtyCount: 0 },
|
|
1446
|
+
document: { open: false, dirty: false, mode: "", canUndo: false, canRedo: false },
|
|
1447
|
+
editor: { kind: "none" },
|
|
1448
|
+
canvas: { view: "design" },
|
|
1449
|
+
pane: { count: 1, derived: false },
|
|
1450
|
+
selection: {
|
|
1451
|
+
count: 0,
|
|
1452
|
+
paths: [],
|
|
1453
|
+
kind: "",
|
|
1454
|
+
isRoot: false,
|
|
1455
|
+
isComponentInstance: false,
|
|
1456
|
+
isLayoutNode: false,
|
|
1457
|
+
isRepeater: false
|
|
1458
|
+
},
|
|
1459
|
+
caret: { active: false, inCanvas: false },
|
|
1460
|
+
focus: { region: "pane" },
|
|
1461
|
+
modal: { open: false },
|
|
1462
|
+
collab: { attached: false, readOnly: false, sourceCanonical: false },
|
|
1463
|
+
ai: { configured: false, streaming: false, waiting: false },
|
|
1464
|
+
capability: {
|
|
1465
|
+
gitClone: false,
|
|
1466
|
+
importSite: false,
|
|
1467
|
+
openProjectInNewWindow: false,
|
|
1468
|
+
dataRows: false,
|
|
1469
|
+
windowControls: false,
|
|
1470
|
+
findReferences: false
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function makeContext(patch = {}) {
|
|
1475
|
+
const base = emptyContext();
|
|
1476
|
+
for (const key of Object.keys(patch)) {
|
|
1477
|
+
const group = patch[key];
|
|
1478
|
+
if (group) {
|
|
1479
|
+
Object.assign(base[key], group);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return base;
|
|
1483
|
+
}
|
|
1484
|
+
var MODAL_STACK = ["palette"];
|
|
1485
|
+
var CARET_STACK = ["caret", "global"];
|
|
1486
|
+
var DOCK_STACK = ["dock", "global"];
|
|
1487
|
+
var CANVAS_STACK = ["canvas", "global"];
|
|
1488
|
+
var GRID_STACK = ["grid", "global"];
|
|
1489
|
+
var CODE_STACK = ["code", "global"];
|
|
1490
|
+
var GLOBAL_STACK = ["global"];
|
|
1491
|
+
function keyScopeStack(ctx) {
|
|
1492
|
+
if (ctx.modal.open) {
|
|
1493
|
+
return MODAL_STACK;
|
|
1494
|
+
}
|
|
1495
|
+
if (ctx.caret.active) {
|
|
1496
|
+
return CARET_STACK;
|
|
1497
|
+
}
|
|
1498
|
+
if (ctx.focus.region !== "pane") {
|
|
1499
|
+
return DOCK_STACK;
|
|
1500
|
+
}
|
|
1501
|
+
switch (ctx.editor.kind) {
|
|
1502
|
+
case "grid": {
|
|
1503
|
+
return GRID_STACK;
|
|
1504
|
+
}
|
|
1505
|
+
case "code": {
|
|
1506
|
+
return CODE_STACK;
|
|
1507
|
+
}
|
|
1508
|
+
case "canvas": {
|
|
1509
|
+
return ctx.canvas.view === "preview" ? GLOBAL_STACK : CANVAS_STACK;
|
|
1510
|
+
}
|
|
1511
|
+
default: {
|
|
1512
|
+
return GLOBAL_STACK;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/tabs/tab.ts
|
|
1518
|
+
var PROJECT_CONFIG_PATH = "project.json";
|
|
1519
|
+
function createDefaultUi(canvasMode, preview = false) {
|
|
1520
|
+
return {
|
|
1521
|
+
activeMedia: null,
|
|
1522
|
+
activeSelector: null,
|
|
1523
|
+
canvasMode,
|
|
1524
|
+
editZoom: 1,
|
|
1525
|
+
editingFormula: null,
|
|
1526
|
+
editingFunction: null,
|
|
1527
|
+
featureToggles: {},
|
|
1528
|
+
inspectorSections: {},
|
|
1529
|
+
pendingInlineEdit: null,
|
|
1530
|
+
preview,
|
|
1531
|
+
previewColorScheme: "auto",
|
|
1532
|
+
previewLocale: null,
|
|
1533
|
+
previewParams: {},
|
|
1534
|
+
previewProps: null,
|
|
1535
|
+
rightTab: "properties",
|
|
1536
|
+
showLayout: true,
|
|
1537
|
+
styleFilter: "",
|
|
1538
|
+
styleSections: {},
|
|
1539
|
+
dataRows: {},
|
|
1540
|
+
dataLimits: {},
|
|
1541
|
+
styleShorthands: {},
|
|
1542
|
+
zoom: 1
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
var ALL_MODES = ["edit", "design", "preview", "source", "stylebook"];
|
|
1546
|
+
function createTab({
|
|
1547
|
+
id,
|
|
1548
|
+
documentPath = null,
|
|
1549
|
+
fileHandle = null,
|
|
1550
|
+
document,
|
|
1551
|
+
frontmatter,
|
|
1552
|
+
sourceFormat = null,
|
|
1553
|
+
capabilities,
|
|
1554
|
+
openedFrom = null,
|
|
1555
|
+
preview: previewTab = false
|
|
1556
|
+
}) {
|
|
1557
|
+
const scope = effectScope();
|
|
1558
|
+
normalizeArrayChildren(document);
|
|
1559
|
+
const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
|
|
1560
|
+
const initialCanvasMode = resolvedModes.find((m) => m !== "preview") ?? "edit";
|
|
1561
|
+
const initialPreview = resolvedModes[0] === "preview";
|
|
1562
|
+
const tab = scope.run(() => ({
|
|
1563
|
+
capabilities: { modes: resolvedModes },
|
|
1564
|
+
doc: reactive({
|
|
1565
|
+
content: { frontmatter: frontmatter || {} },
|
|
1566
|
+
dirty: false,
|
|
1567
|
+
document,
|
|
1568
|
+
handlersSource: null,
|
|
1569
|
+
mode: inferDocumentMode(documentPath, sourceFormat),
|
|
1570
|
+
sourceFormat
|
|
1571
|
+
}),
|
|
1572
|
+
documentPath,
|
|
1573
|
+
fileHandle,
|
|
1574
|
+
history: reactive({
|
|
1575
|
+
index: 0,
|
|
1576
|
+
snapshots: [{ document: structuredClone(document), selection: [] }]
|
|
1577
|
+
}),
|
|
1578
|
+
id,
|
|
1579
|
+
pinned: false,
|
|
1580
|
+
preview: previewTab,
|
|
1581
|
+
scope,
|
|
1582
|
+
session: reactive({
|
|
1583
|
+
canvas: {
|
|
1584
|
+
refreshing: false,
|
|
1585
|
+
error: null,
|
|
1586
|
+
livePreviews: null,
|
|
1587
|
+
pendingInlineEdit: null,
|
|
1588
|
+
scope: null,
|
|
1589
|
+
status: "idle"
|
|
1590
|
+
},
|
|
1591
|
+
clipboard: null,
|
|
1592
|
+
hover: null,
|
|
1593
|
+
openedFrom,
|
|
1594
|
+
selection: [],
|
|
1595
|
+
ui: createDefaultUi(initialCanvasMode, initialPreview)
|
|
1596
|
+
})
|
|
1597
|
+
}));
|
|
1598
|
+
return tab;
|
|
1599
|
+
}
|
|
1600
|
+
function inferModes(documentPath, sourceFormat) {
|
|
1601
|
+
if (documentPath === PROJECT_CONFIG_PATH) {
|
|
1602
|
+
return ["stylebook", "source"];
|
|
1603
|
+
}
|
|
1604
|
+
const format = formatByName(sourceFormat) ?? formatForPath(documentPath);
|
|
1605
|
+
if (format) {
|
|
1606
|
+
return format.studio?.modes ?? ["edit", "design", "preview", "source"];
|
|
1607
|
+
}
|
|
1608
|
+
return ALL_MODES;
|
|
1609
|
+
}
|
|
1610
|
+
function inferDocumentMode(documentPath, sourceFormat) {
|
|
1611
|
+
const format = formatByName(sourceFormat) ?? formatForPath(documentPath);
|
|
1612
|
+
if (format) {
|
|
1613
|
+
return format.studio?.documentMode?.default ?? "content";
|
|
1614
|
+
}
|
|
1615
|
+
return "component";
|
|
1616
|
+
}
|
|
1617
|
+
function editorKindOf(tab) {
|
|
1618
|
+
return editorKindForMode(tab.session.ui.canvasMode);
|
|
1619
|
+
}
|
|
1620
|
+
function editorKindsOf(tab) {
|
|
1621
|
+
const kinds = [];
|
|
1622
|
+
for (const mode of tab.capabilities.modes) {
|
|
1623
|
+
if (mode === "preview") {
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const kind = editorKindForMode(mode);
|
|
1627
|
+
if (!kinds.includes(kind)) {
|
|
1628
|
+
kinds.push(kind);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return kinds;
|
|
1632
|
+
}
|
|
1633
|
+
function modeForEditorKind(tab, kind) {
|
|
1634
|
+
return tab.capabilities.modes.find((mode) => mode !== "preview" && editorKindForMode(mode) === kind);
|
|
1635
|
+
}
|
|
1636
|
+
function disposeTab(tab) {
|
|
1637
|
+
tab.scope.stop();
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/collab/collab-state.ts
|
|
1641
|
+
var states = new WeakMap;
|
|
1642
|
+
var attachedPaths = new Map;
|
|
1643
|
+
function collabState(tab) {
|
|
1644
|
+
const key = toRaw(tab);
|
|
1645
|
+
let state = states.get(key);
|
|
1646
|
+
if (!state) {
|
|
1647
|
+
state = reactive({
|
|
1648
|
+
active: false,
|
|
1649
|
+
attachError: "",
|
|
1650
|
+
peers: [],
|
|
1651
|
+
readOnly: false,
|
|
1652
|
+
sourceCanonical: false,
|
|
1653
|
+
status: "unavailable"
|
|
1654
|
+
});
|
|
1655
|
+
states.set(key, state);
|
|
1656
|
+
}
|
|
1657
|
+
return state;
|
|
1658
|
+
}
|
|
1659
|
+
function registerCollabPath(path) {
|
|
1660
|
+
attachedPaths.set(path, (attachedPaths.get(path) ?? 0) + 1);
|
|
1661
|
+
}
|
|
1662
|
+
function unregisterCollabPath(path) {
|
|
1663
|
+
const count = attachedPaths.get(path) ?? 0;
|
|
1664
|
+
if (count <= 1) {
|
|
1665
|
+
attachedPaths.delete(path);
|
|
1666
|
+
} else {
|
|
1667
|
+
attachedPaths.set(path, count - 1);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function isCollabPath(path) {
|
|
1671
|
+
return attachedPaths.has(path.replaceAll("\\", "/"));
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/collab/collab-session.ts
|
|
1675
|
+
var SYNC_TIMEOUT_MS = 8000;
|
|
1676
|
+
var MIRROR_DEBOUNCE_MS = 800;
|
|
1677
|
+
var MIRROR_MAX_LAG_MS = 2500;
|
|
1678
|
+
var collabModulePromise = null;
|
|
1679
|
+
function loadCollab() {
|
|
1680
|
+
collabModulePromise ??= import("./index-zqxmqxmh.js");
|
|
1681
|
+
return collabModulePromise;
|
|
1682
|
+
}
|
|
1683
|
+
function maybePlatform() {
|
|
1684
|
+
try {
|
|
1685
|
+
return getPlatform();
|
|
1686
|
+
} catch {
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
var _serialize = null;
|
|
1691
|
+
function configureCollabSerializer(fn) {
|
|
1692
|
+
_serialize = fn;
|
|
1693
|
+
}
|
|
1694
|
+
var _parse = null;
|
|
1695
|
+
function configureCollabParser(fn) {
|
|
1696
|
+
_parse = fn;
|
|
1697
|
+
}
|
|
1698
|
+
var _notify = () => {};
|
|
1699
|
+
function configureCollabNotifier(fn) {
|
|
1700
|
+
_notify = fn ?? (() => {});
|
|
1701
|
+
}
|
|
1702
|
+
var runtimes = new WeakMap;
|
|
1703
|
+
function rawTab(tab) {
|
|
1704
|
+
return toRaw(tab);
|
|
1705
|
+
}
|
|
1706
|
+
function runtimeOf(tab) {
|
|
1707
|
+
return runtimes.get(rawTab(tab));
|
|
1708
|
+
}
|
|
1709
|
+
function runtimeFor(tab) {
|
|
1710
|
+
const key = rawTab(tab);
|
|
1711
|
+
let runtime = runtimes.get(key);
|
|
1712
|
+
if (!runtime) {
|
|
1713
|
+
runtime = {
|
|
1714
|
+
attaching: null,
|
|
1715
|
+
generation: 0,
|
|
1716
|
+
optedOut: reactive({ value: false }),
|
|
1717
|
+
session: null,
|
|
1718
|
+
watcherInstalled: false
|
|
1719
|
+
};
|
|
1720
|
+
runtimes.set(key, runtime);
|
|
1721
|
+
}
|
|
1722
|
+
return runtime;
|
|
1723
|
+
}
|
|
1724
|
+
var hooksInstalled = false;
|
|
1725
|
+
function installGlobalHooks() {
|
|
1726
|
+
if (hooksInstalled) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
hooksInstalled = true;
|
|
1730
|
+
setTransactObserver(onTransact);
|
|
1731
|
+
setBatchEndNotifier(onBatchEnd);
|
|
1732
|
+
setTransactGate((tab) => {
|
|
1733
|
+
const session = runtimeOf(tab)?.session;
|
|
1734
|
+
if (session?.synced && collabState(tab).sourceCanonical) {
|
|
1735
|
+
_notify("Source editing in progress — structural edits are paused");
|
|
1736
|
+
return "source-canonical";
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
function onTransact(tab, record, origin) {
|
|
1742
|
+
const session = runtimeOf(tab)?.session;
|
|
1743
|
+
if (!session || !session.synced) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
session.lastSeenRef = toRaw(tab.doc.document);
|
|
1747
|
+
if (origin === "remote") {
|
|
1748
|
+
scheduleMirror(session);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (session.canWrite) {
|
|
1752
|
+
publishRecord(session, record);
|
|
1753
|
+
scheduleMirror(session);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function onBatchEnd(tab) {
|
|
1757
|
+
const session = runtimeOf(tab)?.session;
|
|
1758
|
+
if (!session?.synced || session.batchOps === null) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const ops = session.batchOps;
|
|
1762
|
+
const needsDiff = session.batchNeedsDiff;
|
|
1763
|
+
session.batchOps = null;
|
|
1764
|
+
session.batchNeedsDiff = false;
|
|
1765
|
+
if (!session.canWrite) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
session.undoManager?.stopCapturing();
|
|
1769
|
+
if (needsDiff) {
|
|
1770
|
+
publishDiff(session);
|
|
1771
|
+
} else if (ops.length > 0) {
|
|
1772
|
+
try {
|
|
1773
|
+
session.collab.applyDocOpsToY(session.handle.doc, ops, session.collab.LOCAL_ORIGIN);
|
|
1774
|
+
} catch {
|
|
1775
|
+
publishDiff(session);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
session.undoManager?.stopCapturing();
|
|
1779
|
+
}
|
|
1780
|
+
function publishRecord(session, record) {
|
|
1781
|
+
const forward = record.docOps.map((pair) => pair.forward);
|
|
1782
|
+
if (isBatching()) {
|
|
1783
|
+
session.batchOps ??= [];
|
|
1784
|
+
if (forward.length === 0) {
|
|
1785
|
+
session.batchNeedsDiff = true;
|
|
1786
|
+
} else {
|
|
1787
|
+
session.batchOps.push(...forward);
|
|
1788
|
+
}
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (forward.length === 0) {
|
|
1792
|
+
publishDiff(session);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
try {
|
|
1796
|
+
session.collab.applyDocOpsToY(session.handle.doc, forward, session.collab.LOCAL_ORIGIN);
|
|
1797
|
+
} catch {
|
|
1798
|
+
publishDiff(session);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function publishDiff(session) {
|
|
1802
|
+
const { collab, handle, tab } = session;
|
|
1803
|
+
const before = collab.yDocToJson(handle.doc);
|
|
1804
|
+
const after = jsonClone(toRaw(tab.doc.document));
|
|
1805
|
+
const ops = collab.diffDocs(before, after);
|
|
1806
|
+
if (ops === null) {
|
|
1807
|
+
collab.replaceYStructure(handle.doc, after, collab.LOCAL_ORIGIN);
|
|
1808
|
+
} else if (ops.length > 0) {
|
|
1809
|
+
try {
|
|
1810
|
+
collab.applyDocOpsToY(handle.doc, ops, collab.LOCAL_ORIGIN);
|
|
1811
|
+
} catch {
|
|
1812
|
+
collab.replaceYStructure(handle.doc, after, collab.LOCAL_ORIGIN);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function reconcileTabFromY(session) {
|
|
1817
|
+
const next = session.collab.yDocToJson(session.handle.doc);
|
|
1818
|
+
transactDoc(session.tab, (t) => {
|
|
1819
|
+
const target = t.doc.document;
|
|
1820
|
+
for (const key of Object.keys(target)) {
|
|
1821
|
+
if (!(key in next)) {
|
|
1822
|
+
delete target[key];
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
Object.assign(target, next);
|
|
1826
|
+
}, { origin: "remote", skipHistory: true });
|
|
1827
|
+
}
|
|
1828
|
+
function isReconciler(session) {
|
|
1829
|
+
if (!session.canWrite) {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
const { awareness } = session.handle;
|
|
1833
|
+
let lowest = awareness.clientID;
|
|
1834
|
+
for (const [clientId, state] of awareness.getStates()) {
|
|
1835
|
+
if (state.canWrite !== false && clientId < lowest) {
|
|
1836
|
+
lowest = clientId;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return lowest === session.handle.awareness.clientID;
|
|
1840
|
+
}
|
|
1841
|
+
function scheduleMirror(session) {
|
|
1842
|
+
if (!_serialize || !isReconciler(session)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (session.collab.canonicalOf(session.handle.doc) === "source") {
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const now = Date.now();
|
|
1849
|
+
session.lastMirrorAt ??= now;
|
|
1850
|
+
if (now - session.lastMirrorAt >= MIRROR_MAX_LAG_MS) {
|
|
1851
|
+
if (session.mirrorTimer) {
|
|
1852
|
+
clearTimeout(session.mirrorTimer);
|
|
1853
|
+
session.mirrorTimer = null;
|
|
1854
|
+
}
|
|
1855
|
+
session.lastMirrorAt = now;
|
|
1856
|
+
mirrorNow(session);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (session.mirrorTimer) {
|
|
1860
|
+
clearTimeout(session.mirrorTimer);
|
|
1861
|
+
}
|
|
1862
|
+
session.mirrorTimer = setTimeout(() => {
|
|
1863
|
+
session.mirrorTimer = null;
|
|
1864
|
+
session.lastMirrorAt = Date.now();
|
|
1865
|
+
mirrorNow(session);
|
|
1866
|
+
}, MIRROR_DEBOUNCE_MS);
|
|
1867
|
+
}
|
|
1868
|
+
async function mirrorNow(session) {
|
|
1869
|
+
if (!_serialize || !session.synced) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
const text = await _serialize(session.tab);
|
|
1874
|
+
session.collab.updateSourceText(session.handle.doc, text, session.collab.MIRROR_ORIGIN);
|
|
1875
|
+
} catch {}
|
|
1876
|
+
}
|
|
1877
|
+
async function sourceParseNow(session) {
|
|
1878
|
+
if (!_parse || !session.synced || !session.canWrite) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const { collab, handle, tab } = session;
|
|
1882
|
+
const rev = collab.canonicalRevOf(handle.doc);
|
|
1883
|
+
const text = collab.sourceText(handle.doc).toString();
|
|
1884
|
+
let parsed;
|
|
1885
|
+
try {
|
|
1886
|
+
parsed = await _parse(tab, text);
|
|
1887
|
+
} catch {
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (!session.synced || collab.canonicalRevOf(handle.doc) !== rev) {
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
const ops = collab.diffDocs(collab.yDocToJson(handle.doc), parsed.document);
|
|
1894
|
+
if (ops === null) {
|
|
1895
|
+
collab.replaceYStructure(handle.doc, parsed.document, collab.MIRROR_ORIGIN);
|
|
1896
|
+
} else if (ops.length > 0) {
|
|
1897
|
+
try {
|
|
1898
|
+
collab.applyDocOpsToY(handle.doc, ops, collab.MIRROR_ORIGIN);
|
|
1899
|
+
} catch {
|
|
1900
|
+
collab.replaceYStructure(handle.doc, parsed.document, collab.MIRROR_ORIGIN);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const frontmatter = collab.frontmatterMap(handle.doc);
|
|
1904
|
+
const next = parsed.frontmatter ?? {};
|
|
1905
|
+
handle.doc.transact(() => {
|
|
1906
|
+
const sharedKeys = [...frontmatter.keys()];
|
|
1907
|
+
for (const key of sharedKeys) {
|
|
1908
|
+
if (!(key in next)) {
|
|
1909
|
+
frontmatter.delete(key);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1913
|
+
if (!collab.deepEqual(frontmatter.get(key), value)) {
|
|
1914
|
+
frontmatter.set(key, value);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}, collab.MIRROR_ORIGIN);
|
|
1918
|
+
}
|
|
1919
|
+
function collabSourceContext(tab) {
|
|
1920
|
+
const session = runtimeOf(tab)?.session;
|
|
1921
|
+
if (!session?.synced) {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
const { collab, handle } = session;
|
|
1925
|
+
let claim = null;
|
|
1926
|
+
const holdsClaim = () => claim !== null && session.sourceLockOwner === claim;
|
|
1927
|
+
return {
|
|
1928
|
+
awareness: handle.awareness,
|
|
1929
|
+
enter: async () => {
|
|
1930
|
+
claim = {};
|
|
1931
|
+
session.sourceLockOwner = claim;
|
|
1932
|
+
const local = handle.awareness.getLocalState();
|
|
1933
|
+
if (local) {
|
|
1934
|
+
handle.awareness.setLocalState({ ...local, mode: "source" });
|
|
1935
|
+
}
|
|
1936
|
+
if (!session.canWrite) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
let serialized = null;
|
|
1940
|
+
if (_serialize) {
|
|
1941
|
+
try {
|
|
1942
|
+
serialized = await _serialize(tab);
|
|
1943
|
+
} catch {
|
|
1944
|
+
serialized = null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (!holdsClaim()) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
collab.acquireSourceCanonical(handle.doc, serialized ?? collab.sourceText(handle.doc).toString(), collab.LOCAL_ORIGIN);
|
|
1951
|
+
},
|
|
1952
|
+
leave: () => {
|
|
1953
|
+
if (!holdsClaim()) {
|
|
1954
|
+
claim = null;
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
claim = null;
|
|
1958
|
+
session.sourceLockOwner = null;
|
|
1959
|
+
const local = handle.awareness.getLocalState();
|
|
1960
|
+
if (local) {
|
|
1961
|
+
handle.awareness.setLocalState({ ...local, mode: "structure" });
|
|
1962
|
+
}
|
|
1963
|
+
if (!session.canWrite) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const others = collab.otherSourceEditors(handle.awareness, session.path, handle.awareness.clientID).filter((clientId) => {
|
|
1967
|
+
const peer = handle.awareness.getStates().get(clientId);
|
|
1968
|
+
return peer?.canWrite !== false;
|
|
1969
|
+
});
|
|
1970
|
+
if (others.length === 0) {
|
|
1971
|
+
sourceParseNow(session).then(() => {
|
|
1972
|
+
collab.releaseSourceCanonical(handle.doc, collab.LOCAL_ORIGIN);
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
localOrigin: collab.LOCAL_ORIGIN,
|
|
1977
|
+
readOnly: !session.canWrite,
|
|
1978
|
+
text: collab.sourceText(handle.doc)
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function collabReadOnly(tab) {
|
|
1982
|
+
const state = collabState(tab);
|
|
1983
|
+
return state.active && state.readOnly;
|
|
1984
|
+
}
|
|
1985
|
+
async function collabSave(tab) {
|
|
1986
|
+
const session = runtimeOf(tab)?.session;
|
|
1987
|
+
if (!session?.synced || !session.canWrite) {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
if (session.mirrorTimer) {
|
|
1991
|
+
clearTimeout(session.mirrorTimer);
|
|
1992
|
+
session.mirrorTimer = null;
|
|
1993
|
+
}
|
|
1994
|
+
await mirrorNow(session);
|
|
1995
|
+
await session.handle.flush();
|
|
1996
|
+
return true;
|
|
1997
|
+
}
|
|
1998
|
+
async function flushAllCollab() {
|
|
1999
|
+
for (const tab of liveTabs) {
|
|
2000
|
+
const session = runtimeOf(tab)?.session;
|
|
2001
|
+
if (session?.synced) {
|
|
2002
|
+
await collabSave(tab);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
var liveTabs = new Set;
|
|
2007
|
+
function ensureCollab(tab) {
|
|
2008
|
+
const platform = maybePlatform();
|
|
2009
|
+
if (!platform?.collab || !tab.documentPath || tab.documentPath === PROJECT_CONFIG_PATH) {
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const runtime = runtimeFor(tab);
|
|
2013
|
+
if (runtime.watcherInstalled) {
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
runtime.watcherInstalled = true;
|
|
2017
|
+
installGlobalHooks();
|
|
2018
|
+
liveTabs.add(tab);
|
|
2019
|
+
tab.scope.run(() => {
|
|
2020
|
+
effect(() => {
|
|
2021
|
+
if (runtime.optedOut.value) {
|
|
2022
|
+
detachSession(tab);
|
|
2023
|
+
} else {
|
|
2024
|
+
attachSession(tab);
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
onScopeDispose(() => {
|
|
2028
|
+
liveTabs.delete(tab);
|
|
2029
|
+
detachSession(tab);
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function setCollabEnabled(tab, enabled) {
|
|
2034
|
+
const runtime = runtimeFor(tab);
|
|
2035
|
+
if (runtime.optedOut.value === !enabled) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
runtime.optedOut.value = !enabled;
|
|
2039
|
+
if (!runtime.watcherInstalled && enabled) {
|
|
2040
|
+
ensureCollab(tab);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
function isCollabEnabled(tab) {
|
|
2044
|
+
return runtimeOf(tab)?.optedOut.value !== true;
|
|
2045
|
+
}
|
|
2046
|
+
function rekeyCollab(tab) {
|
|
2047
|
+
const runtime = runtimeOf(tab);
|
|
2048
|
+
if (!runtime?.watcherInstalled) {
|
|
2049
|
+
ensureCollab(tab);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
detachSession(tab);
|
|
2053
|
+
attachSession(tab);
|
|
2054
|
+
}
|
|
2055
|
+
async function attachSession(tab) {
|
|
2056
|
+
const runtime = runtimeFor(tab);
|
|
2057
|
+
if (runtime.session || runtime.attaching) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const { documentPath: path } = tab;
|
|
2061
|
+
const platform = maybePlatform();
|
|
2062
|
+
if (!path || !platform?.collab) {
|
|
2063
|
+
collabState(tab).status = "unavailable";
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const { generation } = runtime;
|
|
2067
|
+
const state = collabState(tab);
|
|
2068
|
+
state.status = "connecting";
|
|
2069
|
+
state.attachError = "";
|
|
2070
|
+
const attempt = (async () => {
|
|
2071
|
+
try {
|
|
2072
|
+
const [collab, handle] = await Promise.all([loadCollab(), platform.collab(path)]);
|
|
2073
|
+
if (!handle) {
|
|
2074
|
+
state.status = "detached";
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (runtime.generation !== generation) {
|
|
2078
|
+
handle.destroy();
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
let timer = null;
|
|
2082
|
+
const timeout = new Promise((_, reject) => {
|
|
2083
|
+
timer = setTimeout(() => reject(new Error("collab-sync-timeout")), SYNC_TIMEOUT_MS);
|
|
2084
|
+
});
|
|
2085
|
+
try {
|
|
2086
|
+
await Promise.race([handle.whenSynced, timeout]);
|
|
2087
|
+
} finally {
|
|
2088
|
+
if (timer) {
|
|
2089
|
+
clearTimeout(timer);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
if (runtime.generation !== generation) {
|
|
2093
|
+
handle.destroy();
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
runtime.session = createSession(tab, path, collab, handle);
|
|
2097
|
+
state.active = true;
|
|
2098
|
+
state.status = "synced";
|
|
2099
|
+
state.readOnly = !runtime.session.canWrite;
|
|
2100
|
+
state.attachError = "";
|
|
2101
|
+
registerCollabPath(path);
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
state.status = "failed";
|
|
2104
|
+
state.active = false;
|
|
2105
|
+
state.attachError = error instanceof Error ? error.message : String(error);
|
|
2106
|
+
notify.warn(`Live collaboration is not available for "${path}" — ${state.attachError}`, {
|
|
2107
|
+
key: `collab:${path}`,
|
|
2108
|
+
path,
|
|
2109
|
+
source: "Collaboration"
|
|
2110
|
+
});
|
|
2111
|
+
} finally {
|
|
2112
|
+
runtime.attaching = null;
|
|
2113
|
+
}
|
|
2114
|
+
})();
|
|
2115
|
+
runtime.attaching = attempt;
|
|
2116
|
+
await attempt;
|
|
2117
|
+
}
|
|
2118
|
+
function detachSession(tab) {
|
|
2119
|
+
const runtime = runtimeOf(tab);
|
|
2120
|
+
if (!runtime) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
runtime.generation += 1;
|
|
2124
|
+
const { session } = runtime;
|
|
2125
|
+
runtime.session = null;
|
|
2126
|
+
const state = collabState(tab);
|
|
2127
|
+
state.active = false;
|
|
2128
|
+
state.status = "detached";
|
|
2129
|
+
state.attachError = "";
|
|
2130
|
+
state.peers = [];
|
|
2131
|
+
if (!session) {
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
unregisterCollabPath(session.path);
|
|
2135
|
+
if (session.mirrorTimer) {
|
|
2136
|
+
clearTimeout(session.mirrorTimer);
|
|
2137
|
+
}
|
|
2138
|
+
if (session.sourceParseTimer) {
|
|
2139
|
+
clearTimeout(session.sourceParseTimer);
|
|
2140
|
+
}
|
|
2141
|
+
for (const dispose of session.disposers) {
|
|
2142
|
+
try {
|
|
2143
|
+
dispose();
|
|
2144
|
+
} catch {}
|
|
2145
|
+
}
|
|
2146
|
+
setHistoryDelegate(tab, null);
|
|
2147
|
+
tab.history.snapshots = [
|
|
2148
|
+
{
|
|
2149
|
+
document: jsonClone(toRaw(tab.doc.document)),
|
|
2150
|
+
selection: cloneSelection(tab.session.selection)
|
|
2151
|
+
}
|
|
2152
|
+
];
|
|
2153
|
+
tab.history.index = 0;
|
|
2154
|
+
session.undoManager?.destroy();
|
|
2155
|
+
session.handle.destroy();
|
|
2156
|
+
}
|
|
2157
|
+
function createSession(tab, path, collab, handle) {
|
|
2158
|
+
const identity = handle.identity();
|
|
2159
|
+
const session = {
|
|
2160
|
+
applyingRemoteFrontmatter: false,
|
|
2161
|
+
batchNeedsDiff: false,
|
|
2162
|
+
batchOps: null,
|
|
2163
|
+
canWrite: identity ? identity.permission !== "read" : true,
|
|
2164
|
+
collab,
|
|
2165
|
+
disposers: [],
|
|
2166
|
+
handle,
|
|
2167
|
+
lastSeenRef: toRaw(tab.doc.document),
|
|
2168
|
+
mirrorTimer: null,
|
|
2169
|
+
path,
|
|
2170
|
+
sourceLockOwner: null,
|
|
2171
|
+
sourceParseTimer: null,
|
|
2172
|
+
synced: false,
|
|
2173
|
+
tab,
|
|
2174
|
+
undoManager: null
|
|
2175
|
+
};
|
|
2176
|
+
const meta = collab.metaMap(handle.doc);
|
|
2177
|
+
if (meta.get("structureSeeded") === true) {
|
|
2178
|
+
const yJson = collab.yDocToJson(handle.doc);
|
|
2179
|
+
const tabJson = jsonClone(toRaw(tab.doc.document));
|
|
2180
|
+
if (!collab.deepEqual(yJson, tabJson)) {
|
|
2181
|
+
const ops = collab.diffDocs(tabJson, yJson);
|
|
2182
|
+
if (ops === null) {
|
|
2183
|
+
session.synced = true;
|
|
2184
|
+
reconcileTabFromY(session);
|
|
2185
|
+
session.synced = false;
|
|
2186
|
+
} else if (ops.length > 0) {
|
|
2187
|
+
applyExternalDocOps(tab, ops);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
session.applyingRemoteFrontmatter = true;
|
|
2191
|
+
const sharedFrontmatter = collab.frontmatterMap(handle.doc);
|
|
2192
|
+
for (const [key, value] of sharedFrontmatter.entries()) {
|
|
2193
|
+
tab.doc.content.frontmatter[key] = value;
|
|
2194
|
+
}
|
|
2195
|
+
session.applyingRemoteFrontmatter = false;
|
|
2196
|
+
} else if (session.canWrite) {
|
|
2197
|
+
collab.seedStructure(handle.doc, jsonClone(toRaw(tab.doc.document)), {
|
|
2198
|
+
frontmatter: jsonClone(toRaw(tab.doc.content.frontmatter)),
|
|
2199
|
+
origin: collab.SEED_ORIGIN,
|
|
2200
|
+
sourceFormat: tab.doc.sourceFormat
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
session.lastSeenRef = toRaw(tab.doc.document);
|
|
2204
|
+
session.disposers.push(handle.onDirty((dirty) => {
|
|
2205
|
+
tab.doc.dirty = dirty;
|
|
2206
|
+
}));
|
|
2207
|
+
handle.awareness.setLocalState({
|
|
2208
|
+
canWrite: session.canWrite,
|
|
2209
|
+
focusedPath: path,
|
|
2210
|
+
structuralSelection: null,
|
|
2211
|
+
user: identity ? {
|
|
2212
|
+
avatarUrl: identity.avatarUrl,
|
|
2213
|
+
color: identity.color,
|
|
2214
|
+
login: identity.login,
|
|
2215
|
+
name: identity.name
|
|
2216
|
+
} : {
|
|
2217
|
+
color: collab.colorForKey(String(handle.awareness.clientID)),
|
|
2218
|
+
login: `guest-${handle.awareness.clientID % 1000}`
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
const undoManager = new collab.UndoManager([collab.structureMap(handle.doc), collab.frontmatterMap(handle.doc)], { trackedOrigins: new Set([collab.LOCAL_ORIGIN]) });
|
|
2222
|
+
session.undoManager = undoManager;
|
|
2223
|
+
const onStackAdd = ({ stackItem }) => {
|
|
2224
|
+
stackItem.meta.set("selection", cloneSelection(tab.session.selection));
|
|
2225
|
+
};
|
|
2226
|
+
const onStackPop = ({ stackItem }) => {
|
|
2227
|
+
const selection = stackItem.meta.get("selection");
|
|
2228
|
+
tab.session.selection = cloneSelection(selection ?? []);
|
|
2229
|
+
};
|
|
2230
|
+
undoManager.on("stack-item-added", onStackAdd);
|
|
2231
|
+
undoManager.on("stack-item-popped", onStackPop);
|
|
2232
|
+
session.disposers.push(() => {
|
|
2233
|
+
undoManager.off("stack-item-added", onStackAdd);
|
|
2234
|
+
undoManager.off("stack-item-popped", onStackPop);
|
|
2235
|
+
});
|
|
2236
|
+
setHistoryDelegate(tab, {
|
|
2237
|
+
canRedo: () => undoManager.canRedo(),
|
|
2238
|
+
canUndo: () => undoManager.canUndo(),
|
|
2239
|
+
redo: () => {
|
|
2240
|
+
undoManager.redo();
|
|
2241
|
+
},
|
|
2242
|
+
undo: () => {
|
|
2243
|
+
undoManager.undo();
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
const structure = collab.structureMap(handle.doc);
|
|
2247
|
+
const structureObserver = (events, transaction) => {
|
|
2248
|
+
if (transaction.origin === collab.LOCAL_ORIGIN || !session.synced) {
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const ops = collab.yEventsToDocOps(events);
|
|
2252
|
+
if (ops === null) {
|
|
2253
|
+
reconcileTabFromY(session);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (ops.length === 0) {
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
try {
|
|
2260
|
+
applyExternalDocOps(tab, ops);
|
|
2261
|
+
} catch {
|
|
2262
|
+
reconcileTabFromY(session);
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
structure.observeDeep(structureObserver);
|
|
2266
|
+
session.disposers.push(() => structure.unobserveDeep(structureObserver));
|
|
2267
|
+
const tabState = collabState(tab);
|
|
2268
|
+
tabState.sourceCanonical = collab.canonicalOf(handle.doc) === "source";
|
|
2269
|
+
const lockMeta = collab.metaMap(handle.doc);
|
|
2270
|
+
const lockObserver = () => {
|
|
2271
|
+
tabState.sourceCanonical = collab.canonicalOf(handle.doc) === "source";
|
|
2272
|
+
};
|
|
2273
|
+
lockMeta.observe(lockObserver);
|
|
2274
|
+
session.disposers.push(() => lockMeta.unobserve(lockObserver));
|
|
2275
|
+
const source = collab.sourceText(handle.doc);
|
|
2276
|
+
const sourceObserver = (_event, transaction) => {
|
|
2277
|
+
if (transaction.origin === collab.MIRROR_ORIGIN || !session.synced) {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (collab.canonicalOf(handle.doc) !== "source") {
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (!collab.isSourceReconciler(handle.awareness, session.path)) {
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (session.sourceParseTimer) {
|
|
2287
|
+
clearTimeout(session.sourceParseTimer);
|
|
2288
|
+
}
|
|
2289
|
+
session.sourceParseTimer = setTimeout(() => {
|
|
2290
|
+
session.sourceParseTimer = null;
|
|
2291
|
+
sourceParseNow(session);
|
|
2292
|
+
}, 600);
|
|
2293
|
+
};
|
|
2294
|
+
source.observe(sourceObserver);
|
|
2295
|
+
session.disposers.push(() => source.unobserve(sourceObserver));
|
|
2296
|
+
const frontmatter = collab.frontmatterMap(handle.doc);
|
|
2297
|
+
const frontmatterObserver = (event, transaction) => {
|
|
2298
|
+
if (transaction.origin === collab.LOCAL_ORIGIN || !session.synced) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
session.applyingRemoteFrontmatter = true;
|
|
2302
|
+
try {
|
|
2303
|
+
for (const [key, change] of event.changes.keys) {
|
|
2304
|
+
if (change.action === "delete") {
|
|
2305
|
+
delete tab.doc.content.frontmatter[key];
|
|
2306
|
+
} else {
|
|
2307
|
+
tab.doc.content.frontmatter[key] = frontmatter.get(key);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
} finally {
|
|
2311
|
+
session.applyingRemoteFrontmatter = false;
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
frontmatter.observe(frontmatterObserver);
|
|
2315
|
+
session.disposers.push(() => frontmatter.unobserve(frontmatterObserver));
|
|
2316
|
+
tab.scope.run(() => {
|
|
2317
|
+
let lastJson = JSON.stringify(tab.doc.content.frontmatter);
|
|
2318
|
+
effect(() => {
|
|
2319
|
+
const json = JSON.stringify(tab.doc.content.frontmatter);
|
|
2320
|
+
if (json === lastJson) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
lastJson = json;
|
|
2324
|
+
if (session.applyingRemoteFrontmatter || !session.synced || !session.canWrite) {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const local = JSON.parse(json);
|
|
2328
|
+
handle.doc.transact(() => {
|
|
2329
|
+
const sharedKeys = [...frontmatter.keys()];
|
|
2330
|
+
for (const key of sharedKeys) {
|
|
2331
|
+
if (!(key in local)) {
|
|
2332
|
+
frontmatter.delete(key);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
for (const [key, value] of Object.entries(local)) {
|
|
2336
|
+
if (!collab.deepEqual(frontmatter.get(key), value)) {
|
|
2337
|
+
frontmatter.set(key, value);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}, collab.LOCAL_ORIGIN);
|
|
2341
|
+
scheduleMirror(session);
|
|
2342
|
+
});
|
|
2343
|
+
effect(() => {
|
|
2344
|
+
const structuralSelection = tab.session.selection.length > 0 ? cloneSelection(tab.session.selection) : null;
|
|
2345
|
+
const local = handle.awareness.getLocalState();
|
|
2346
|
+
if (local) {
|
|
2347
|
+
handle.awareness.setLocalState({ ...local, structuralSelection });
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
effect(() => {
|
|
2351
|
+
const ref = toRaw(tab.doc.document);
|
|
2352
|
+
queueMicrotask(() => {
|
|
2353
|
+
const current = runtimeOf(tab)?.session;
|
|
2354
|
+
if (current !== session || !session.synced || !session.canWrite) {
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const nowRef = toRaw(tab.doc.document);
|
|
2358
|
+
if (nowRef !== ref || nowRef === session.lastSeenRef) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
session.lastSeenRef = nowRef;
|
|
2362
|
+
publishDiff(session);
|
|
2363
|
+
scheduleMirror(session);
|
|
2364
|
+
});
|
|
2365
|
+
});
|
|
2366
|
+
});
|
|
2367
|
+
const state = collabState(tab);
|
|
2368
|
+
const awarenessObserver = () => {
|
|
2369
|
+
const peers = [];
|
|
2370
|
+
for (const [clientId, peerState] of handle.awareness.getStates()) {
|
|
2371
|
+
if (clientId !== handle.awareness.clientID && peerState["user"]) {
|
|
2372
|
+
peers.push({ clientId, state: peerState });
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
state.peers = peers;
|
|
2376
|
+
};
|
|
2377
|
+
awarenessObserver();
|
|
2378
|
+
handle.awareness.on("change", awarenessObserver);
|
|
2379
|
+
session.disposers.push(() => handle.awareness.off("change", awarenessObserver), handle.onStatus((status) => {
|
|
2380
|
+
if (status === "offline" || status === "connecting") {
|
|
2381
|
+
state.status = "offline";
|
|
2382
|
+
} else if (session.synced) {
|
|
2383
|
+
state.status = "synced";
|
|
2384
|
+
}
|
|
2385
|
+
}), handle.onReset(() => {
|
|
2386
|
+
detachSession(tab);
|
|
2387
|
+
attachSession(tab);
|
|
2388
|
+
}));
|
|
2389
|
+
session.synced = true;
|
|
2390
|
+
return session;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/commands/command-args.ts
|
|
2394
|
+
function refuse(commandId, key, problem) {
|
|
2395
|
+
return new RangeError(`command "${commandId}" argument "${key}": ${problem}`);
|
|
2396
|
+
}
|
|
2397
|
+
function describe(value) {
|
|
2398
|
+
if (value === undefined) {
|
|
2399
|
+
return "missing";
|
|
2400
|
+
}
|
|
2401
|
+
return typeof value === "string" ? `"${value}"` : `${typeof value} ${JSON.stringify(value)}`;
|
|
2402
|
+
}
|
|
2403
|
+
function stringArg(commandId, args, key) {
|
|
2404
|
+
const value = args[key];
|
|
2405
|
+
if (typeof value !== "string" || value === "") {
|
|
2406
|
+
throw refuse(commandId, key, `expected a non-empty string, got ${describe(value)}`);
|
|
2407
|
+
}
|
|
2408
|
+
return value;
|
|
2409
|
+
}
|
|
2410
|
+
function optionalStringArg(commandId, args, key) {
|
|
2411
|
+
if (args[key] === undefined) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
return stringArg(commandId, args, key);
|
|
2415
|
+
}
|
|
2416
|
+
function numberArg(commandId, args, key) {
|
|
2417
|
+
const value = args[key];
|
|
2418
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2419
|
+
throw refuse(commandId, key, `expected a finite number, got ${describe(value)}`);
|
|
2420
|
+
}
|
|
2421
|
+
return value;
|
|
2422
|
+
}
|
|
2423
|
+
function boundedNumberArg(commandId, args, key, minimum, maximum) {
|
|
2424
|
+
const value = numberArg(commandId, args, key);
|
|
2425
|
+
if (value < minimum || value > maximum) {
|
|
2426
|
+
throw refuse(commandId, key, `${value} is outside the supported range ${minimum}–${maximum}`);
|
|
2427
|
+
}
|
|
2428
|
+
return value;
|
|
2429
|
+
}
|
|
2430
|
+
function booleanArg(commandId, args, key) {
|
|
2431
|
+
const value = args[key];
|
|
2432
|
+
if (typeof value !== "boolean") {
|
|
2433
|
+
throw refuse(commandId, key, `expected a boolean, got ${describe(value)}`);
|
|
2434
|
+
}
|
|
2435
|
+
return value;
|
|
2436
|
+
}
|
|
2437
|
+
function enumArg(commandId, args, key, declared) {
|
|
2438
|
+
const value = args[key];
|
|
2439
|
+
if (typeof value !== "string" || !declared.includes(value)) {
|
|
2440
|
+
throw refuse(commandId, key, `${describe(value)} is not declared — declared: ${declared.join(", ")}`);
|
|
2441
|
+
}
|
|
2442
|
+
return value;
|
|
2443
|
+
}
|
|
2444
|
+
function pathArg(commandId, args, key) {
|
|
2445
|
+
const value = args[key];
|
|
2446
|
+
if (!Array.isArray(value) || !value.every((s) => typeof s === "string" || typeof s === "number")) {
|
|
2447
|
+
throw refuse(commandId, key, `expected an array of path segments (strings or numbers), got ${describe(value)}`);
|
|
2448
|
+
}
|
|
2449
|
+
return value;
|
|
2450
|
+
}
|
|
2451
|
+
function nullablePathArg(commandId, args, key) {
|
|
2452
|
+
if (args[key] === null) {
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
return pathArg(commandId, args, key);
|
|
2456
|
+
}
|
|
2457
|
+
function pathListArg(commandId, args, key) {
|
|
2458
|
+
const value = args[key];
|
|
2459
|
+
if (!Array.isArray(value)) {
|
|
2460
|
+
throw refuse(commandId, key, `expected an array of document paths, got ${describe(value)}`);
|
|
2461
|
+
}
|
|
2462
|
+
return value.map((entry, i) => {
|
|
2463
|
+
if (!Array.isArray(entry) || !entry.every((s) => typeof s === "string" || typeof s === "number")) {
|
|
2464
|
+
throw refuse(commandId, key, `entry ${i} is not a document path — expected an array of segments, got ${describe(entry)}`);
|
|
2465
|
+
}
|
|
2466
|
+
return entry;
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
function argsSchema(properties, required = Object.keys(properties)) {
|
|
2470
|
+
return {
|
|
2471
|
+
additionalProperties: false,
|
|
2472
|
+
properties,
|
|
2473
|
+
required: [...required],
|
|
2474
|
+
type: "object"
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function enumProperty(declared, description) {
|
|
2478
|
+
return { description, enum: [...declared], type: "string" };
|
|
2479
|
+
}
|
|
2480
|
+
function derivedEnumProperty(declared, description) {
|
|
2481
|
+
return {
|
|
2482
|
+
description,
|
|
2483
|
+
get enum() {
|
|
2484
|
+
return [...declared()];
|
|
2485
|
+
},
|
|
2486
|
+
type: "string"
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
function numberProperty(description, bounds = {}) {
|
|
2490
|
+
return { description, type: "number", ...bounds };
|
|
2491
|
+
}
|
|
2492
|
+
function booleanProperty(description) {
|
|
2493
|
+
return { description, type: "boolean" };
|
|
2494
|
+
}
|
|
2495
|
+
function pathProperty(description, nullable = false) {
|
|
2496
|
+
const path = {
|
|
2497
|
+
items: { type: ["string", "number"] },
|
|
2498
|
+
type: "array"
|
|
2499
|
+
};
|
|
2500
|
+
return nullable ? { description, oneOf: [path, { type: "null" }] } : { description, ...path };
|
|
2501
|
+
}
|
|
2502
|
+
function pathListProperty(description) {
|
|
2503
|
+
return {
|
|
2504
|
+
description,
|
|
2505
|
+
items: { items: { type: ["string", "number"] }, type: "array" },
|
|
2506
|
+
type: "array"
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
function stringProperty(description) {
|
|
2510
|
+
return { description, type: "string" };
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/workspace/workspace.ts
|
|
2514
|
+
var CLOSED_TAB_LIMIT = 20;
|
|
2515
|
+
var PRIMARY_PANE = "primary";
|
|
2516
|
+
var SECONDARY_PANE = "secondary";
|
|
2517
|
+
var MAX_PANES = 2;
|
|
2518
|
+
var workspace = reactive({
|
|
2519
|
+
activePaneId: PRIMARY_PANE,
|
|
2520
|
+
clipboard: null,
|
|
2521
|
+
closedTabs: [],
|
|
2522
|
+
componentRegistry: [],
|
|
2523
|
+
mruOrder: [],
|
|
2524
|
+
fileTree: {
|
|
2525
|
+
dirs: new Map,
|
|
2526
|
+
expanded: new Set,
|
|
2527
|
+
searchQuery: "",
|
|
2528
|
+
selectedPath: null
|
|
2529
|
+
},
|
|
2530
|
+
panes: [
|
|
2531
|
+
{
|
|
2532
|
+
activeTabId: null,
|
|
2533
|
+
derived: null,
|
|
2534
|
+
id: PRIMARY_PANE,
|
|
2535
|
+
tabOrder: []
|
|
2536
|
+
}
|
|
2537
|
+
],
|
|
2538
|
+
projectConfig: null,
|
|
2539
|
+
projectRoot: null,
|
|
2540
|
+
styleClipboard: null,
|
|
2541
|
+
tabs: new Map,
|
|
2542
|
+
ui: {
|
|
2543
|
+
activityBar: "files"
|
|
2544
|
+
},
|
|
2545
|
+
get activeTabId() {
|
|
2546
|
+
const pane = activePane();
|
|
2547
|
+
return pane.derived?.kind === "lens" ? paneById(pane.derived.sourcePaneId)?.activeTabId ?? null : pane.activeTabId;
|
|
2548
|
+
},
|
|
2549
|
+
get tabOrder() {
|
|
2550
|
+
return activePane().tabOrder;
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
var activeTab = computed(() => workspace.activeTabId ? workspace.tabs.get(workspace.activeTabId) ?? null : null);
|
|
2554
|
+
function activePane() {
|
|
2555
|
+
const { panes } = workspace;
|
|
2556
|
+
return panes.find((pane) => pane.id === workspace.activePaneId) ?? panes[0];
|
|
2557
|
+
}
|
|
2558
|
+
function paneById(paneId) {
|
|
2559
|
+
return workspace.panes.find((pane) => pane.id === paneId);
|
|
2560
|
+
}
|
|
2561
|
+
function paneOfTab(tabId) {
|
|
2562
|
+
return workspace.panes.find((pane) => pane.tabOrder.includes(tabId));
|
|
2563
|
+
}
|
|
2564
|
+
function focusPane(paneId) {
|
|
2565
|
+
const pane = paneById(paneId);
|
|
2566
|
+
if (!pane || pane.id === workspace.activePaneId) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
workspace.activePaneId = pane.id;
|
|
2570
|
+
resetTabCycle();
|
|
2571
|
+
const { activeTabId } = pane;
|
|
2572
|
+
if (activeTabId) {
|
|
2573
|
+
promoteMru(activeTabId);
|
|
2574
|
+
const tab = workspace.tabs.get(activeTabId);
|
|
2575
|
+
if (tab) {
|
|
2576
|
+
syncTreeSelection(tab);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function isPaneFocused(paneId) {
|
|
2581
|
+
return workspace.activePaneId === paneId;
|
|
2582
|
+
}
|
|
2583
|
+
function focusOtherPane() {
|
|
2584
|
+
const other = workspace.panes.find((pane) => pane.id !== workspace.activePaneId);
|
|
2585
|
+
if (other) {
|
|
2586
|
+
focusPane(other.id);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
function splitRight() {
|
|
2590
|
+
const source = activePane();
|
|
2591
|
+
const tabId = source.activeTabId;
|
|
2592
|
+
const tab = tabId ? workspace.tabs.get(tabId) : null;
|
|
2593
|
+
if (!tabId || !tab) {
|
|
2594
|
+
return null;
|
|
2595
|
+
}
|
|
2596
|
+
const target = receivingPane();
|
|
2597
|
+
detachTab(tabId);
|
|
2598
|
+
insertIntoPane(target, tabId);
|
|
2599
|
+
target.activeTabId = tabId;
|
|
2600
|
+
workspace.activePaneId = target.id;
|
|
2601
|
+
resetTabCycle();
|
|
2602
|
+
promoteMru(tabId);
|
|
2603
|
+
return target;
|
|
2604
|
+
}
|
|
2605
|
+
function sidePane() {
|
|
2606
|
+
const source = activePane();
|
|
2607
|
+
return workspace.panes.find((pane) => pane.id !== source.id) ?? addPane(SECONDARY_PANE);
|
|
2608
|
+
}
|
|
2609
|
+
function receivingPane() {
|
|
2610
|
+
const target = sidePane();
|
|
2611
|
+
if (target.derived) {
|
|
2612
|
+
target.derived = null;
|
|
2613
|
+
}
|
|
2614
|
+
return target;
|
|
2615
|
+
}
|
|
2616
|
+
function addPane(paneId) {
|
|
2617
|
+
const existing = paneById(paneId);
|
|
2618
|
+
if (existing) {
|
|
2619
|
+
return existing;
|
|
2620
|
+
}
|
|
2621
|
+
workspace.panes = [
|
|
2622
|
+
...workspace.panes,
|
|
2623
|
+
{
|
|
2624
|
+
activeTabId: null,
|
|
2625
|
+
derived: null,
|
|
2626
|
+
id: paneId,
|
|
2627
|
+
tabOrder: []
|
|
2628
|
+
}
|
|
2629
|
+
];
|
|
2630
|
+
return paneById(paneId);
|
|
2631
|
+
}
|
|
2632
|
+
function closePane(paneId) {
|
|
2633
|
+
if (workspace.panes.length < 2) {
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
const closingId = paneId === PRIMARY_PANE ? workspace.panes.find((candidate) => candidate.id !== PRIMARY_PANE)?.id : paneId;
|
|
2637
|
+
const pane = closingId === undefined ? undefined : paneById(closingId);
|
|
2638
|
+
const survivor = workspace.panes.find((candidate) => candidate.id !== closingId);
|
|
2639
|
+
if (!pane || !survivor) {
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
for (const tabId of pane.tabOrder) {
|
|
2643
|
+
insertIntoPane(survivor, tabId);
|
|
2644
|
+
}
|
|
2645
|
+
survivor.activeTabId = workspace.activePaneId === pane.id ? pane.activeTabId ?? survivor.activeTabId : survivor.activeTabId ?? pane.activeTabId;
|
|
2646
|
+
workspace.activePaneId = survivor.id;
|
|
2647
|
+
if (survivor.derived?.sourcePaneId === pane.id) {
|
|
2648
|
+
survivor.derived = null;
|
|
2649
|
+
}
|
|
2650
|
+
workspace.panes = workspace.panes.filter((candidate) => candidate.id !== pane.id);
|
|
2651
|
+
resetTabCycle();
|
|
2652
|
+
}
|
|
2653
|
+
function paneIsEmpty(pane) {
|
|
2654
|
+
return pane.tabOrder.length === 0 && pane.derived === null;
|
|
2655
|
+
}
|
|
2656
|
+
function detachTab(tabId) {
|
|
2657
|
+
const emptied = [];
|
|
2658
|
+
for (const pane of workspace.panes) {
|
|
2659
|
+
if (!pane.tabOrder.includes(tabId)) {
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
pane.tabOrder = pane.tabOrder.filter((id) => id !== tabId);
|
|
2663
|
+
if (pane.activeTabId === tabId) {
|
|
2664
|
+
pane.activeTabId = pane.tabOrder.at(-1) ?? null;
|
|
2665
|
+
}
|
|
2666
|
+
if (pane.id !== PRIMARY_PANE && paneIsEmpty(pane)) {
|
|
2667
|
+
emptied.push(pane.id);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
for (const paneId of emptied) {
|
|
2671
|
+
closePane(paneId);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function insertIntoPane(pane, tabId, index) {
|
|
2675
|
+
if (pane.tabOrder.includes(tabId)) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const at = index ?? defaultSlot(pane, tabId);
|
|
2679
|
+
pane.tabOrder = [...pane.tabOrder.slice(0, at), tabId, ...pane.tabOrder.slice(at)];
|
|
2680
|
+
}
|
|
2681
|
+
function pinnedCount(pane) {
|
|
2682
|
+
let count = 0;
|
|
2683
|
+
for (const id of pane.tabOrder) {
|
|
2684
|
+
if (workspace.tabs.get(id)?.pinned !== true) {
|
|
2685
|
+
break;
|
|
2686
|
+
}
|
|
2687
|
+
count += 1;
|
|
2688
|
+
}
|
|
2689
|
+
return count;
|
|
2690
|
+
}
|
|
2691
|
+
function defaultSlot(pane, tabId) {
|
|
2692
|
+
return workspace.tabs.get(tabId)?.pinned === true ? pinnedCount(pane) : pane.tabOrder.length;
|
|
2693
|
+
}
|
|
2694
|
+
function setWorkspaceProject(root, config = null) {
|
|
2695
|
+
workspace.projectRoot = root;
|
|
2696
|
+
workspace.projectConfig = config;
|
|
2697
|
+
}
|
|
2698
|
+
function isTabActive(tab) {
|
|
2699
|
+
const active = activeTab.value;
|
|
2700
|
+
return tab !== null && active !== null && toRaw(active) === toRaw(tab);
|
|
2701
|
+
}
|
|
2702
|
+
function tabIsLive(tab) {
|
|
2703
|
+
if (!tab) {
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
const held = workspace.tabs.get(tab.id);
|
|
2707
|
+
return held !== undefined && toRaw(held) === toRaw(tab);
|
|
2708
|
+
}
|
|
2709
|
+
function promoteMru(tabId) {
|
|
2710
|
+
workspace.mruOrder = [tabId, ...workspace.mruOrder.filter((id) => id !== tabId)];
|
|
2711
|
+
}
|
|
2712
|
+
var _cycleList = null;
|
|
2713
|
+
var _cycleIndex = 0;
|
|
2714
|
+
function resetTabCycle() {
|
|
2715
|
+
_cycleList = null;
|
|
2716
|
+
_cycleIndex = 0;
|
|
2717
|
+
}
|
|
2718
|
+
function syncTreeSelection(tab) {
|
|
2719
|
+
if (projectState && tab.documentPath) {
|
|
2720
|
+
projectState.selectedPath = tab.documentPath;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
function setActiveTab(tabId, opts = {}) {
|
|
2724
|
+
const tab = workspace.tabs.get(tabId);
|
|
2725
|
+
const pane = paneOfTab(tabId);
|
|
2726
|
+
if (!tab || !pane) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
pane.activeTabId = tabId;
|
|
2730
|
+
if (opts.focus === false) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
workspace.activePaneId = pane.id;
|
|
2734
|
+
if (!opts.cycling) {
|
|
2735
|
+
resetTabCycle();
|
|
2736
|
+
promoteMru(tabId);
|
|
2737
|
+
}
|
|
2738
|
+
syncTreeSelection(tab);
|
|
2739
|
+
}
|
|
2740
|
+
function openTab(opts) {
|
|
2741
|
+
const previous = workspace.tabs.get(opts.id);
|
|
2742
|
+
const preview = opts.preview === true && previous?.pinned !== true;
|
|
2743
|
+
const tab = createTab({ ...opts, preview });
|
|
2744
|
+
const pane = (opts.paneId === undefined ? undefined : paneById(opts.paneId)) ?? activePane();
|
|
2745
|
+
let slot;
|
|
2746
|
+
if (previous) {
|
|
2747
|
+
tab.pinned = previous.pinned;
|
|
2748
|
+
disposeTab(previous);
|
|
2749
|
+
} else {
|
|
2750
|
+
if (preview) {
|
|
2751
|
+
const sitting = previewTabIn(pane);
|
|
2752
|
+
const sittingTab = sitting ? workspace.tabs.get(sitting) : null;
|
|
2753
|
+
if (sittingTab) {
|
|
2754
|
+
commitTabBuffers(sittingTab);
|
|
2755
|
+
}
|
|
2756
|
+
promoteDirtyPreviewTabs();
|
|
2757
|
+
}
|
|
2758
|
+
const replaced = preview ? previewTabIn(pane) : null;
|
|
2759
|
+
if (replaced) {
|
|
2760
|
+
slot = pane.tabOrder.indexOf(replaced);
|
|
2761
|
+
closeTab(replaced);
|
|
2762
|
+
}
|
|
2763
|
+
insertIntoPane(pane, tab.id, slot);
|
|
2764
|
+
}
|
|
2765
|
+
workspace.tabs.set(tab.id, tab);
|
|
2766
|
+
setActiveTab(tab.id, { focus: opts.focus !== false });
|
|
2767
|
+
ensureCollab(tab);
|
|
2768
|
+
return tab;
|
|
2769
|
+
}
|
|
2770
|
+
function previewTabIn(pane) {
|
|
2771
|
+
return pane.tabOrder.find((id) => workspace.tabs.get(id)?.preview === true) ?? null;
|
|
2772
|
+
}
|
|
2773
|
+
function promoteTab(tabId) {
|
|
2774
|
+
const tab = workspace.tabs.get(tabId);
|
|
2775
|
+
if (tab?.preview === true) {
|
|
2776
|
+
tab.preview = false;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
function promoteDirtyPreviewTabs() {
|
|
2780
|
+
for (const [id, tab] of workspace.tabs) {
|
|
2781
|
+
if (tab.preview && tab.doc.dirty) {
|
|
2782
|
+
promoteTab(id);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function setTabPinned(tabId, pinned) {
|
|
2787
|
+
const tab = workspace.tabs.get(tabId);
|
|
2788
|
+
const pane = paneOfTab(tabId);
|
|
2789
|
+
if (!tab || !pane) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
tab.pinned = pinned;
|
|
2793
|
+
if (pinned) {
|
|
2794
|
+
tab.preview = false;
|
|
2795
|
+
}
|
|
2796
|
+
const without = pane.tabOrder.filter((id) => id !== tabId);
|
|
2797
|
+
const boundary = without.filter((id) => workspace.tabs.get(id)?.pinned === true).length;
|
|
2798
|
+
pane.tabOrder = [...without.slice(0, boundary), tabId, ...without.slice(boundary)];
|
|
2799
|
+
}
|
|
2800
|
+
function moveTab(tabId, toIndex) {
|
|
2801
|
+
const tab = workspace.tabs.get(tabId);
|
|
2802
|
+
const pane = paneOfTab(tabId);
|
|
2803
|
+
if (!tab || !pane) {
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
const without = pane.tabOrder.filter((id) => id !== tabId);
|
|
2807
|
+
const boundary = without.filter((id) => workspace.tabs.get(id)?.pinned === true).length;
|
|
2808
|
+
const lower = tab.pinned ? 0 : boundary;
|
|
2809
|
+
const upper = tab.pinned ? boundary : without.length;
|
|
2810
|
+
const at = Math.min(Math.max(toIndex, lower), upper);
|
|
2811
|
+
pane.tabOrder = [...without.slice(0, at), tabId, ...without.slice(at)];
|
|
2812
|
+
}
|
|
2813
|
+
function closeTab(tabId) {
|
|
2814
|
+
const tab = workspace.tabs.get(tabId);
|
|
2815
|
+
if (!tab) {
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
const pane = paneOfTab(tabId);
|
|
2819
|
+
const wasActive = pane?.activeTabId === tabId;
|
|
2820
|
+
rememberClosedTab(tab);
|
|
2821
|
+
disposeTab(tab);
|
|
2822
|
+
workspace.tabs.delete(tabId);
|
|
2823
|
+
detachTab(tabId);
|
|
2824
|
+
workspace.mruOrder = workspace.mruOrder.filter((id) => id !== tabId);
|
|
2825
|
+
resetTabCycle();
|
|
2826
|
+
if (pane && paneIsEmpty(pane)) {
|
|
2827
|
+
closePane(pane.id);
|
|
2828
|
+
}
|
|
2829
|
+
if (!wasActive) {
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const survivor = activePane();
|
|
2833
|
+
const next = workspace.mruOrder.find((id) => survivor.tabOrder.includes(id)) ?? survivor.tabOrder.at(-1) ?? null;
|
|
2834
|
+
if (next) {
|
|
2835
|
+
setActiveTab(next);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
function closeAllTabs() {
|
|
2839
|
+
const tabs = [...workspace.tabs.values()];
|
|
2840
|
+
workspace.tabs.clear();
|
|
2841
|
+
resetPanes();
|
|
2842
|
+
workspace.mruOrder = [];
|
|
2843
|
+
resetTabCycle();
|
|
2844
|
+
for (const tab of tabs) {
|
|
2845
|
+
disposeTab(tab);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
function resetPanes() {
|
|
2849
|
+
workspace.panes = [{ activeTabId: null, derived: null, id: PRIMARY_PANE, tabOrder: [] }];
|
|
2850
|
+
workspace.activePaneId = PRIMARY_PANE;
|
|
2851
|
+
}
|
|
2852
|
+
function rememberClosedTab(tab) {
|
|
2853
|
+
if (!tab.documentPath) {
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const path = tab.documentPath;
|
|
2857
|
+
workspace.closedTabs = [
|
|
2858
|
+
{ documentPath: path },
|
|
2859
|
+
...workspace.closedTabs.filter((entry) => entry.documentPath !== path)
|
|
2860
|
+
].slice(0, CLOSED_TAB_LIMIT);
|
|
2861
|
+
}
|
|
2862
|
+
function takeClosedTabPath() {
|
|
2863
|
+
const [entry] = workspace.closedTabs;
|
|
2864
|
+
if (!entry) {
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
workspace.closedTabs = workspace.closedTabs.slice(1);
|
|
2868
|
+
return entry.documentPath;
|
|
2869
|
+
}
|
|
2870
|
+
function cycleTab(step) {
|
|
2871
|
+
if (!_cycleList) {
|
|
2872
|
+
_cycleList = workspace.mruOrder.filter((id2) => workspace.tabs.has(id2));
|
|
2873
|
+
_cycleIndex = 0;
|
|
2874
|
+
}
|
|
2875
|
+
const list = _cycleList;
|
|
2876
|
+
if (list.length < 2) {
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
_cycleIndex = ((_cycleIndex + step) % list.length + list.length) % list.length;
|
|
2880
|
+
const id = list[_cycleIndex];
|
|
2881
|
+
setActiveTab(id, { cycling: true });
|
|
2882
|
+
return id;
|
|
2883
|
+
}
|
|
2884
|
+
function endTabCycle() {
|
|
2885
|
+
const settled = workspace.activeTabId;
|
|
2886
|
+
resetTabCycle();
|
|
2887
|
+
if (settled) {
|
|
2888
|
+
promoteMru(settled);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function replaceAllTabs(newTabOpts) {
|
|
2892
|
+
const oldIds = [...workspace.tabs.keys()];
|
|
2893
|
+
const oldTabs = [...workspace.tabs.values()];
|
|
2894
|
+
const newTab = createTab(newTabOpts);
|
|
2895
|
+
workspace.tabs.set(newTab.id, newTab);
|
|
2896
|
+
resetPanes();
|
|
2897
|
+
workspace.panes[0].tabOrder = [newTab.id];
|
|
2898
|
+
workspace.mruOrder = [];
|
|
2899
|
+
resetTabCycle();
|
|
2900
|
+
setActiveTab(newTab.id);
|
|
2901
|
+
ensureCollab(newTab);
|
|
2902
|
+
for (const id of oldIds) {
|
|
2903
|
+
if (id === newTab.id) {
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
workspace.tabs.delete(id);
|
|
2907
|
+
}
|
|
2908
|
+
for (const tab of oldTabs) {
|
|
2909
|
+
disposeTab(tab);
|
|
2910
|
+
}
|
|
2911
|
+
return newTab;
|
|
2912
|
+
}
|
|
2913
|
+
function activateTab(tabId, opts = {}) {
|
|
2914
|
+
setActiveTab(tabId, opts);
|
|
2915
|
+
}
|
|
2916
|
+
function moveTabToPane(tabId, paneId) {
|
|
2917
|
+
const pane = paneById(paneId);
|
|
2918
|
+
if (!pane || !workspace.tabs.has(tabId)) {
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
if (pane.tabOrder.includes(tabId)) {
|
|
2922
|
+
return pane;
|
|
2923
|
+
}
|
|
2924
|
+
detachTab(tabId);
|
|
2925
|
+
insertIntoPane(pane, tabId);
|
|
2926
|
+
return pane;
|
|
2927
|
+
}
|
|
2928
|
+
function renameTab(oldId, newId, newDocumentPath) {
|
|
2929
|
+
const tab = workspace.tabs.get(oldId);
|
|
2930
|
+
if (!tab) {
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
tab.id = newId;
|
|
2934
|
+
tab.documentPath = newDocumentPath;
|
|
2935
|
+
workspace.tabs.delete(oldId);
|
|
2936
|
+
workspace.tabs.set(newId, tab);
|
|
2937
|
+
for (const pane of workspace.panes) {
|
|
2938
|
+
pane.tabOrder = pane.tabOrder.map((id) => id === oldId ? newId : id);
|
|
2939
|
+
if (pane.activeTabId === oldId) {
|
|
2940
|
+
pane.activeTabId = newId;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
workspace.mruOrder = workspace.mruOrder.map((id) => id === oldId ? newId : id);
|
|
2944
|
+
resetTabCycle();
|
|
2945
|
+
rekeyCollab(tab);
|
|
2946
|
+
}
|
|
2947
|
+
function registerTabCommands(registry, deps) {
|
|
2948
|
+
registry.registerAll([...tabCommands(deps), ...paneCommands(deps)]);
|
|
2949
|
+
}
|
|
2950
|
+
function tabCommands(deps) {
|
|
2951
|
+
const twoOrMoreTabs = () => workspace.tabs.size > 1;
|
|
2952
|
+
return [
|
|
2953
|
+
{
|
|
2954
|
+
id: "document.nextTab",
|
|
2955
|
+
title: "Next Tab",
|
|
2956
|
+
category: "Document",
|
|
2957
|
+
level: "document",
|
|
2958
|
+
keybinding: ["ctrl+tab", "mod+tab"],
|
|
2959
|
+
menus: ["palette"],
|
|
2960
|
+
group: "2_navigate",
|
|
2961
|
+
when: (ctx) => ctx.document.open,
|
|
2962
|
+
enablement: twoOrMoreTabs,
|
|
2963
|
+
requires: "a second open document",
|
|
2964
|
+
run: () => {
|
|
2965
|
+
cycleTab(1);
|
|
2966
|
+
}
|
|
2967
|
+
},
|
|
2968
|
+
{
|
|
2969
|
+
id: "document.previousTab",
|
|
2970
|
+
title: "Previous Tab",
|
|
2971
|
+
category: "Document",
|
|
2972
|
+
level: "document",
|
|
2973
|
+
keybinding: ["ctrl+shift+tab", "mod+shift+tab"],
|
|
2974
|
+
menus: ["palette"],
|
|
2975
|
+
group: "2_navigate",
|
|
2976
|
+
when: (ctx) => ctx.document.open,
|
|
2977
|
+
enablement: twoOrMoreTabs,
|
|
2978
|
+
requires: "a second open document",
|
|
2979
|
+
run: () => {
|
|
2980
|
+
cycleTab(-1);
|
|
2981
|
+
}
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
id: "document.reopenClosed",
|
|
2985
|
+
title: "Reopen Closed Document",
|
|
2986
|
+
category: "Document",
|
|
2987
|
+
level: "document",
|
|
2988
|
+
keybinding: "mod+shift+t",
|
|
2989
|
+
menus: ["context/tab", "palette"],
|
|
2990
|
+
group: "1_file",
|
|
2991
|
+
enablement: () => workspace.closedTabs.length > 0,
|
|
2992
|
+
requires: "a document closed in this session",
|
|
2993
|
+
run: async () => {
|
|
2994
|
+
const path = takeClosedTabPath();
|
|
2995
|
+
if (path) {
|
|
2996
|
+
await deps.openFile(path);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
id: "document.togglePinned",
|
|
3002
|
+
title: "Pin / Unpin Document",
|
|
3003
|
+
category: "Document",
|
|
3004
|
+
level: "document",
|
|
3005
|
+
menus: ["context/tab", "palette"],
|
|
3006
|
+
group: "1_file",
|
|
3007
|
+
when: (ctx) => ctx.document.open,
|
|
3008
|
+
requires: "an open document",
|
|
3009
|
+
run: () => {
|
|
3010
|
+
const id = workspace.activeTabId;
|
|
3011
|
+
const tab = id ? workspace.tabs.get(id) : null;
|
|
3012
|
+
if (id && tab) {
|
|
3013
|
+
setTabPinned(id, !tab.pinned);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
args: argsSchema({
|
|
3019
|
+
pinned: booleanProperty("True to pin the active document's tab, false to unpin it.")
|
|
3020
|
+
}),
|
|
3021
|
+
id: "document.setPinned",
|
|
3022
|
+
title: "Set Document Pinned",
|
|
3023
|
+
category: "Document",
|
|
3024
|
+
level: "document",
|
|
3025
|
+
menus: ["palette"],
|
|
3026
|
+
group: "1_file",
|
|
3027
|
+
when: (ctx) => ctx.document.open,
|
|
3028
|
+
requires: "an open document",
|
|
3029
|
+
run: (_ctx, args) => {
|
|
3030
|
+
const id = workspace.activeTabId;
|
|
3031
|
+
if (id && workspace.tabs.get(id)) {
|
|
3032
|
+
setTabPinned(id, booleanArg("document.setPinned", args, "pinned"));
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
id: "document.keepOpen",
|
|
3038
|
+
title: "Keep Document Open",
|
|
3039
|
+
category: "Document",
|
|
3040
|
+
level: "document",
|
|
3041
|
+
menus: ["context/tab", "palette"],
|
|
3042
|
+
group: "1_file",
|
|
3043
|
+
when: (ctx) => ctx.document.open,
|
|
3044
|
+
enablement: () => {
|
|
3045
|
+
const id = workspace.activeTabId;
|
|
3046
|
+
return id !== null && workspace.tabs.get(id)?.preview === true;
|
|
3047
|
+
},
|
|
3048
|
+
requires: "a preview document — one opened by a single click",
|
|
3049
|
+
run: () => {
|
|
3050
|
+
const id = workspace.activeTabId;
|
|
3051
|
+
if (id) {
|
|
3052
|
+
promoteTab(id);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
];
|
|
3057
|
+
}
|
|
3058
|
+
function paneCommands(deps) {
|
|
3059
|
+
const twoPanes = () => workspace.panes.length > 1;
|
|
3060
|
+
return [
|
|
3061
|
+
{
|
|
3062
|
+
id: "pane.splitRight",
|
|
3063
|
+
title: "Split Right",
|
|
3064
|
+
category: "View",
|
|
3065
|
+
level: "document",
|
|
3066
|
+
keybinding: "mod+\\",
|
|
3067
|
+
menus: ["context/pane", "context/tab", "palette"],
|
|
3068
|
+
group: "5_pane",
|
|
3069
|
+
when: (ctx) => ctx.document.open,
|
|
3070
|
+
enablement: () => workspace.activeTabId !== null,
|
|
3071
|
+
requires: "an open document",
|
|
3072
|
+
undo: "none",
|
|
3073
|
+
run: () => {
|
|
3074
|
+
splitRight();
|
|
3075
|
+
}
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
args: argsSchema({
|
|
3079
|
+
path: stringProperty("Project-relative path of the document to open beside this one.")
|
|
3080
|
+
}),
|
|
3081
|
+
id: "pane.compareWith",
|
|
3082
|
+
title: "Compare With…",
|
|
3083
|
+
category: "View",
|
|
3084
|
+
level: "document",
|
|
3085
|
+
menus: ["context/pane", "context/tab", "palette"],
|
|
3086
|
+
group: "5_pane",
|
|
3087
|
+
when: (ctx) => ctx.document.open,
|
|
3088
|
+
enablement: () => workspace.activeTabId !== null,
|
|
3089
|
+
requires: "an open document",
|
|
3090
|
+
undo: "none",
|
|
3091
|
+
aiTool: {
|
|
3092
|
+
description: "Open a second document in the pane beside the current one, without moving the document " + "you are in.",
|
|
3093
|
+
name: "compare_with"
|
|
3094
|
+
},
|
|
3095
|
+
run: async (_ctx, args) => {
|
|
3096
|
+
const path = stringArg("pane.compareWith", args, "path");
|
|
3097
|
+
const here = activePane();
|
|
3098
|
+
const held = here.activeTabId ? workspace.tabs.get(here.activeTabId) : null;
|
|
3099
|
+
if (held?.documentPath === path) {
|
|
3100
|
+
throw new RangeError(`command "pane.compareWith" argument "path": "${path}" is the document this pane is ` + `already showing — name a different one`);
|
|
3101
|
+
}
|
|
3102
|
+
await deps.openFileInPane(receivingPane().id, path);
|
|
3103
|
+
}
|
|
3104
|
+
},
|
|
3105
|
+
{
|
|
3106
|
+
id: "pane.focusPrimary",
|
|
3107
|
+
title: "Focus Primary Pane",
|
|
3108
|
+
category: "View",
|
|
3109
|
+
level: "document",
|
|
3110
|
+
keybinding: "mod+0",
|
|
3111
|
+
menus: ["palette"],
|
|
3112
|
+
group: "5_pane",
|
|
3113
|
+
enablement: twoPanes,
|
|
3114
|
+
requires: "a split pane grid",
|
|
3115
|
+
undo: "none",
|
|
3116
|
+
run: () => {
|
|
3117
|
+
focusPane(PRIMARY_PANE);
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
id: "pane.focusSecondary",
|
|
3122
|
+
title: "Focus Side Pane",
|
|
3123
|
+
category: "View",
|
|
3124
|
+
level: "document",
|
|
3125
|
+
keybinding: "mod+alt+0",
|
|
3126
|
+
menus: ["palette"],
|
|
3127
|
+
group: "5_pane",
|
|
3128
|
+
enablement: twoPanes,
|
|
3129
|
+
requires: "a split pane grid",
|
|
3130
|
+
undo: "none",
|
|
3131
|
+
run: () => {
|
|
3132
|
+
focusPane(SECONDARY_PANE);
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
id: "pane.unsplit",
|
|
3137
|
+
title: "Close Side Pane",
|
|
3138
|
+
category: "View",
|
|
3139
|
+
level: "document",
|
|
3140
|
+
menus: ["context/pane", "palette"],
|
|
3141
|
+
group: "5_pane",
|
|
3142
|
+
enablement: twoPanes,
|
|
3143
|
+
requires: "a second pane",
|
|
3144
|
+
undo: "none",
|
|
3145
|
+
run: runUnsplit
|
|
3146
|
+
}
|
|
3147
|
+
];
|
|
3148
|
+
}
|
|
3149
|
+
function runUnsplit() {
|
|
3150
|
+
const derived = workspace.panes.find((pane) => pane.derived !== null);
|
|
3151
|
+
if (derived?.id === PRIMARY_PANE) {
|
|
3152
|
+
derived.derived = null;
|
|
3153
|
+
if (!paneIsEmpty(derived)) {
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
closePane(derived?.id ?? SECONDARY_PANE);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
export { LANGUAGE_TAG_PATTERN, canonicalizeLocale, isWellFormedLocale, localeDirection, localeLabel, resolveI18n, localeOfPath, servedLocaleOfPath, translationKeyOfPath, translationPathFor, EDITOR_KIND_LABELS, editorKindForMode, canvasViewForMode, inCanvas, hasSelection, hasElementSelection, emptyContext, makeContext, keyScopeStack, PROJECT_CONFIG_PATH, createTab, editorKindOf, editorKindsOf, modeForEditorKind, disposeTab, primarySelection, selectionAnchor, isSelected, cloneSelection, toggleSelected, rangeSelection, isSpliceablePath, structuralBatch, uniquePaths, unifyValues, setPatchConsumer, recordDocOp, transactDoc, transact, beginBatch, endBatch, isBatching, batchTab, setHistoryDelegate, canUndo, canRedo, undo, redo, mutateInsertNode, mutateRemoveNode, mutateDuplicateNode, mutateRemoveNodes, mutateDuplicateNodes, mutateWrapNode, mutateMoveNode, mutateUpdateProperty, mutateUpdateStyle, mutateUpdateAttribute, mutateUpdateMediaStyle, mutateUpdateNestedStyle, mutateUpdateMediaNestedStyle, mutateUpdateNestedStylePath, mutateUpdateMediaNestedStylePath, mutateReplaceStyle, mutateAddDef, mutateRemoveDef, mutateUpdateDef, mutateRenameDef, mutateUpdateProp, mutateAddSwitchCase, mutateRemoveSwitchCase, mutateRenameSwitchCase, mutateUpdateFrontmatter, collabState, isCollabPath, stringArg, optionalStringArg, boundedNumberArg, booleanArg, enumArg, pathArg, nullablePathArg, pathListArg, argsSchema, enumProperty, derivedEnumProperty, numberProperty, booleanProperty, pathProperty, pathListProperty, stringProperty, LOCALE_PLACEHOLDER, collectionForDirectory, collectionForFile, camelToKebab, camelToLabel, toCamelCase, kebabToLabel, propLabel, attrLabel, abbreviateValue, isMediaFormat, inferInputType, findContentTypeSchema, friendlyNameToVar, varDisplayName, parseCemType, jsonClone, configureCollabSerializer, configureCollabParser, configureCollabNotifier, collabSourceContext, collabReadOnly, collabSave, flushAllCollab, setCollabEnabled, isCollabEnabled, PRIMARY_PANE, SECONDARY_PANE, MAX_PANES, workspace, activeTab, activePane, paneById, paneOfTab, focusPane, isPaneFocused, focusOtherPane, splitRight, sidePane, receivingPane, closePane, detachTab, insertIntoPane, setWorkspaceProject, isTabActive, tabIsLive, openTab, promoteTab, promoteDirtyPreviewTabs, setTabPinned, moveTab, closeTab, closeAllTabs, takeClosedTabPath, cycleTab, endTabCycle, replaceAllTabs, activateTab, moveTabToPane, renameTab, registerTabCommands, tabCommands, paneCommands };
|
|
3161
|
+
|
|
3162
|
+
//# debugId=FC372971F7D8792C64756E2164756E21
|
|
3163
|
+
//# sourceMappingURL=studio-2zkz8evn.js.map
|