@jxsuite/studio 2.0.1 → 2.1.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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived panes (§18.4) — the pane beside this one, showing a projection of it.
|
|
3
|
+
*
|
|
4
|
+
* **Two families, one field.** `Pane.derived` carries either a LENS or a COMPANION, and the
|
|
5
|
+
* distinction is not cosmetic — it is what keeps §14.1 intact:
|
|
6
|
+
*
|
|
7
|
+
* - A **lens** (`code`, `diff`, `breakpoint`) draws the SAME document the source pane owns, in a
|
|
8
|
+
* mode, at a breakpoint or against a diff of its own. It owns no tab at all, so no id maps to two
|
|
9
|
+
* documents and no document takes two ids. `canvas-surface.ts`'s `tabOfPane` takes one hop
|
|
10
|
+
* through `sourcePaneId`, and every pane-scoped renderer in the package starts working on it for
|
|
11
|
+
* free — which is also why the follow costs nothing: switching tabs in the source pane repaints
|
|
12
|
+
* the lens because it is the same reactive read.
|
|
13
|
+
* - A **companion** (`layout`, `component`) shows a DIFFERENT document chosen by a rule from the
|
|
14
|
+
* source. That is an ordinary path-keyed tab this pane owns; the derivation only decides which of
|
|
15
|
+
* its tabs is on screen, which is what activation has always done.
|
|
16
|
+
*
|
|
17
|
+
* §14.1 is therefore SHARPENED rather than amended: **ownership is one-to-one, display is
|
|
18
|
+
* many-to-one.** A tab id appears in at most one pane's `tabOrder`, and every verb that closes,
|
|
19
|
+
* disposes, reorders, pins or MRU-tracks a tab resolves through `paneOfTab` — so it addresses the
|
|
20
|
+
* owner and only the owner. `closeTab`, `detachTab`, `setTabPinned`, `moveTab`, `renameTab`,
|
|
21
|
+
* `rememberClosedTab`, `ensureCollab`, `promoteMru` and `openFileInTab`'s dedupe need no change at
|
|
22
|
+
* all: none of them can see a lens pane, whose `tabOrder` is empty.
|
|
23
|
+
*
|
|
24
|
+
* **What is NOT here, and why.** No derived Tab of any kind — no `derived:code:<path>` id sharing a
|
|
25
|
+
* document, no off-map `Map<paneId, Tab>`. Both mint a second tab LIFETIME: the first costs four
|
|
26
|
+
* exclusion predicates (MRU, reopen-closed, collab keying, close-all counting) that must each be
|
|
27
|
+
* written and tested rather than assumed; the second costs a second disposal path, where one missed
|
|
28
|
+
* `disposeTab` is an `effectScope` reacting to a document forever. Neither is needed once ownership
|
|
29
|
+
* and display are separated.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { effect } from "../reactivity";
|
|
33
|
+
import { componentRegistry } from "../files/components";
|
|
34
|
+
import { getNodeAtPath, parentElementPath } from "../store";
|
|
35
|
+
|
|
36
|
+
import { canvasPerf } from "../canvas/canvas-perf";
|
|
37
|
+
import { canvasModeOfTab, tabOfPane } from "../canvas/canvas-surface";
|
|
38
|
+
import { parseMediaEntries } from "../utils/canvas-media";
|
|
39
|
+
import { getEffectiveLayoutPath, getEffectiveLocales, getEffectiveMedia } from "../site-context";
|
|
40
|
+
import { localeLabel, translationPathFor } from "@jxsuite/schema/locale";
|
|
41
|
+
import { shell } from "../shell";
|
|
42
|
+
import {
|
|
43
|
+
activePane,
|
|
44
|
+
detachTab,
|
|
45
|
+
insertIntoPane,
|
|
46
|
+
paneById,
|
|
47
|
+
promoteTab,
|
|
48
|
+
sidePane,
|
|
49
|
+
workspace,
|
|
50
|
+
} from "./workspace";
|
|
51
|
+
import {
|
|
52
|
+
argsSchema,
|
|
53
|
+
enumArg,
|
|
54
|
+
enumProperty,
|
|
55
|
+
optionalStringArg,
|
|
56
|
+
stringProperty,
|
|
57
|
+
} from "../commands/command-args";
|
|
58
|
+
|
|
59
|
+
import type { DerivationStatus, Pane, PaneDerivation } from "./workspace";
|
|
60
|
+
import type { AnyCommand } from "../commands/registry";
|
|
61
|
+
import type { Tab } from "../tabs/tab";
|
|
62
|
+
import type { GitDiffState, GitFileStatus } from "../types";
|
|
63
|
+
import type { JxPath } from "../state";
|
|
64
|
+
|
|
65
|
+
/** Every projection `pane.derive` offers, in the order the preset menu lists them. */
|
|
66
|
+
export const DERIVE_PRESETS = [
|
|
67
|
+
"code",
|
|
68
|
+
"layout",
|
|
69
|
+
"component",
|
|
70
|
+
"diff",
|
|
71
|
+
"breakpoint",
|
|
72
|
+
"locale",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
export type DerivePreset = (typeof DERIVE_PRESETS)[number];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The canvas mode a preset needs the SOURCE document to DECLARE, for the presets that need one.
|
|
79
|
+
*
|
|
80
|
+
* `tab.capabilities.modes` is the format's own answer to "what can this document be shown as", and
|
|
81
|
+
* the preset menu consulted it nowhere: over a document that declares `["settings", "stylebook",
|
|
82
|
+
* "source"]` it still offered "Same page at Base" — a lens whose stage can only draw an empty
|
|
83
|
+
* artboard — and "Component definition", whose rule can never resolve because nothing in a settings
|
|
84
|
+
* form is a component instance. Both produced a pane the author then had to close.
|
|
85
|
+
*
|
|
86
|
+
* Two presets are absent, for two different reasons. `diff` needs no mode: a comparison is two
|
|
87
|
+
* texts and every document has those, and it is already gated on the file having changes. `layout`
|
|
88
|
+
* is gated on the layout RESOLVING, which {@link presetRefusal} checks directly.
|
|
89
|
+
*
|
|
90
|
+
* **That second gate is NOT a mode check in disguise, and a comment here said it was** — "a
|
|
91
|
+
* document with no design board has no layout either". It is false: `getEffectiveLayoutPath` falls
|
|
92
|
+
* back to `projectConfig.defaults.layout`, which every starter sets, so a document declaring
|
|
93
|
+
* `["stylebook", "source"]` resolves the project's layout and IS offered the row. That is a
|
|
94
|
+
* deliberate answer rather than an oversight — the companion opens a real layout document the
|
|
95
|
+
* author can edit, unlike the empty artboard `breakpoint` would draw or the rule `component` could
|
|
96
|
+
* never resolve — but it is a fact about the project's default, not about this document, so it is
|
|
97
|
+
* pinned by a test (`per-preset refusals` · "Layout's gate is the layout RESOLVING") instead of
|
|
98
|
+
* being asserted here in prose.
|
|
99
|
+
*/
|
|
100
|
+
const MODE_FOR_PRESET: Readonly<Partial<Record<DerivePreset, string>>> = {
|
|
101
|
+
breakpoint: "design",
|
|
102
|
+
code: "source",
|
|
103
|
+
component: "design",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** How a mode is spelled in a refusal — the words on the Editor axis, not the internal ids. */
|
|
107
|
+
const MODE_WORDS: Readonly<Record<string, string>> = { design: "Design", source: "Code" };
|
|
108
|
+
|
|
109
|
+
/** Said by a diff lens whose comparison came back empty. One sentence, one home. */
|
|
110
|
+
const DIFF_UNREADABLE = "Could not read this file's comparison against HEAD.";
|
|
111
|
+
|
|
112
|
+
/** What the derivation commands and the follow need from the rest of Studio. */
|
|
113
|
+
export interface DerivationDeps {
|
|
114
|
+
/**
|
|
115
|
+
* `files/files.ts`'s `openFileInPane` — read a document from disk and show it in a named pane,
|
|
116
|
+
* browsing rather than committing and leaving the keyboard where it is.
|
|
117
|
+
*
|
|
118
|
+
* Injected rather than imported so this module owns no I/O and no test has to stand up a platform
|
|
119
|
+
* to prove the follow memoises.
|
|
120
|
+
*/
|
|
121
|
+
openFileInPane: (paneId: string, path: string) => void | Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Read one file's comparison against HEAD — `gitShow(HEAD)` plus the working copy.
|
|
124
|
+
*
|
|
125
|
+
* **The diff lens has to load its own, and that is the whole of finding 4.** It used to snapshot
|
|
126
|
+
* `shell.git.diffState`: an app-level slot written only when the author clicks a file IN THE GIT
|
|
127
|
+
* PANEL, carrying THAT file's path and never re-read. So the preset was refused for a page that
|
|
128
|
+
* genuinely has changes, and when it was offered it could render a comparison of an unrelated
|
|
129
|
+
* file under a label promising this pane's document. A lens draws the source pane's document or
|
|
130
|
+
* it is not a lens.
|
|
131
|
+
*
|
|
132
|
+
* Resolves to `null` when the comparison cannot be produced; the pane says so rather than drawing
|
|
133
|
+
* somebody else's file.
|
|
134
|
+
*/
|
|
135
|
+
loadDiff: (path: string, fileStatus: string) => Promise<GitDiffState | null>;
|
|
136
|
+
/**
|
|
137
|
+
* Is there a file at this project-relative path? The locale companion's one question.
|
|
138
|
+
*
|
|
139
|
+
* Injected for the reason {@link openFileInPane} is, and asked at all for a sharper one:
|
|
140
|
+
* {@link companionTarget} is pure and synchronous, so it cannot stat anything — and a companion
|
|
141
|
+
* that answers with the path a translation WOULD have hands `openFileInPane` a file that is not
|
|
142
|
+
* there, which fails into a blank pane under a chip naming a language. The pane has to be able to
|
|
143
|
+
* say "there is no French copy of this yet", and only a read of the disk can tell it that.
|
|
144
|
+
*/
|
|
145
|
+
fileExists: (path: string) => Promise<boolean>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A dependency set that does nothing — for callers that read DECLARATIONS rather than run verbs.
|
|
150
|
+
*
|
|
151
|
+
* The sibling of `commands/defaults.ts`'s `noopCommandDeps()`, and here for the same reason: the
|
|
152
|
+
* command-level check, the chrome budget and the generated keyboard sheet all build the whole
|
|
153
|
+
* command set to inspect its records, and every one of them would otherwise hand-write a stub for a
|
|
154
|
+
* verb it will never call. One stub, named, beside the interface it satisfies.
|
|
155
|
+
*
|
|
156
|
+
* @returns {DerivationDeps}
|
|
157
|
+
*/
|
|
158
|
+
export function noopDerivationDeps(): DerivationDeps {
|
|
159
|
+
return {
|
|
160
|
+
fileExists: () => Promise.resolve(false),
|
|
161
|
+
loadDiff: () => Promise.resolve(null),
|
|
162
|
+
openFileInPane: () => {},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The label each preset takes in the menu and in the derived pane's own strip chip. */
|
|
167
|
+
export const PRESET_LABELS: Readonly<Record<DerivePreset, string>> = {
|
|
168
|
+
breakpoint: "Same page at",
|
|
169
|
+
code: "Code",
|
|
170
|
+
component: "Component definition",
|
|
171
|
+
diff: "Diff vs HEAD",
|
|
172
|
+
layout: "Layout",
|
|
173
|
+
/* An UNFINISHED sentence, like `breakpoint`'s, because the row and the chip both finish it with
|
|
174
|
+
the locale's own autonym — "Same page in français". Jx has no message catalogue (§13.3): a
|
|
175
|
+
translation is a different file in a different directory, so this preset opens that file
|
|
176
|
+
rather than re-rendering this one, and the label says "the same page" about the document
|
|
177
|
+
rather than about the text on screen. */
|
|
178
|
+
locale: "Same page in",
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ─── Reading ──────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The derivation a pane is drawing under, or null.
|
|
185
|
+
*
|
|
186
|
+
* Re-exported from `canvas/canvas-surface.ts`, which is where the pane-scoped RENDERERS already
|
|
187
|
+
* resolve everything else and which this module imports from. One name, one answer.
|
|
188
|
+
*/
|
|
189
|
+
export { derivationOfPane } from "../canvas/canvas-surface";
|
|
190
|
+
|
|
191
|
+
/** What a derived pane SHOULD be showing right now. The pure half of the follow. */
|
|
192
|
+
export interface DerivedTarget {
|
|
193
|
+
kind: "lens" | "companion";
|
|
194
|
+
/** Lens: the canvas mode this pane draws in. Null for a companion. */
|
|
195
|
+
mode: string | null;
|
|
196
|
+
/** Lens (`breakpoint`): the media name, null for base. */
|
|
197
|
+
media: string | null;
|
|
198
|
+
/** Companion: the path this pane should show, or null to HOLD the one it has. */
|
|
199
|
+
path: string | null;
|
|
200
|
+
/**
|
|
201
|
+
* Lens (`diff`): the document whose comparison this pane must still LOAD, or null when it has the
|
|
202
|
+
* right one. The impure half acts on it; resolving it stays a read.
|
|
203
|
+
*/
|
|
204
|
+
diffPath: string | null;
|
|
205
|
+
/**
|
|
206
|
+
* Companion (`locale`): the translation whose EXISTENCE this pane must still establish, or null
|
|
207
|
+
* when the answer is already in hand. {@link diffPath}'s companion twin, and for the same reason —
|
|
208
|
+
* the resolver decides WHAT to ask, {@link probeTranslationFor} asks it, and neither answer
|
|
209
|
+
* carries a `probePath`, so the follow cannot re-enter.
|
|
210
|
+
*/
|
|
211
|
+
probePath: string | null;
|
|
212
|
+
/**
|
|
213
|
+
* Companion (`layout`): the node to select once the document is on screen, or null.
|
|
214
|
+
*
|
|
215
|
+
* The deleted `openLayoutAtNode` existed precisely because "Open Layout →" dropped the author
|
|
216
|
+
* into a layout file with nothing selected and left them to find the header again by eye. The
|
|
217
|
+
* derivation carries it now — which is what `panels/properties-panel.ts`'s ledger entry claimed
|
|
218
|
+
* for a release while nothing read `layoutPath` at all.
|
|
219
|
+
*/
|
|
220
|
+
select: JxPath | null;
|
|
221
|
+
status: DerivationStatus;
|
|
222
|
+
/** Printed by the pane when `status` is `unavailable`. Empty otherwise. */
|
|
223
|
+
reason: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve what `paneId` should be showing, from its derivation and its source pane.
|
|
228
|
+
*
|
|
229
|
+
* **Pure**: it reads state and returns an answer, writes nothing, and schedules nothing. That is
|
|
230
|
+
* what makes the follow testable with no rAF, no platform and no canvas — the whole of the
|
|
231
|
+
* derivation's decision-making is one function returning a value.
|
|
232
|
+
*
|
|
233
|
+
* "State" includes {@link _diffLoads}, which is module-scoped rather than on the derivation — a
|
|
234
|
+
* read, and still no write. A comparison that came back empty has to be REMEMBERED somewhere the
|
|
235
|
+
* resolver can see, because a terminal answer written straight onto `derived.status` is overwritten
|
|
236
|
+
* by the next frame's resolve, and the pane then says "Loading…" forever while asking for nothing.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} paneId
|
|
239
|
+
* @returns {DerivedTarget | null} Null when this pane is not derived.
|
|
240
|
+
*/
|
|
241
|
+
export function derivedTarget(paneId: string): DerivedTarget | null {
|
|
242
|
+
const derived = paneById(paneId)?.derived;
|
|
243
|
+
if (!derived) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const source = tabOfPane(derived.sourcePaneId);
|
|
247
|
+
if (!source) {
|
|
248
|
+
return {
|
|
249
|
+
diffPath: null,
|
|
250
|
+
kind: derived.kind,
|
|
251
|
+
media: null,
|
|
252
|
+
mode: null,
|
|
253
|
+
path: null,
|
|
254
|
+
probePath: null,
|
|
255
|
+
reason: "The pane this one follows has no document open.",
|
|
256
|
+
select: null,
|
|
257
|
+
status: "unavailable",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (derived.kind === "lens") {
|
|
261
|
+
return lensTarget(paneId, derived, source);
|
|
262
|
+
}
|
|
263
|
+
return companionTarget(paneId, derived, source);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** A lens's target: a mode, and for `breakpoint` a media the source document actually declares. */
|
|
267
|
+
function lensTarget(
|
|
268
|
+
paneId: string,
|
|
269
|
+
derived: Extract<PaneDerivation, { kind: "lens" }>,
|
|
270
|
+
source: Tab,
|
|
271
|
+
): DerivedTarget {
|
|
272
|
+
const base = {
|
|
273
|
+
diffPath: null,
|
|
274
|
+
kind: "lens" as const,
|
|
275
|
+
media: derived.media,
|
|
276
|
+
mode: derived.mode,
|
|
277
|
+
path: null,
|
|
278
|
+
probePath: null,
|
|
279
|
+
select: null,
|
|
280
|
+
};
|
|
281
|
+
if (derived.preset === "diff") {
|
|
282
|
+
/* THE SOURCE PANE'S DOCUMENT, resolved every time rather than snapshotted once. This branch
|
|
283
|
+
used to read `derived.diff === null`, a copy of `shell.git.diffState` taken when the lens
|
|
284
|
+
was made — so the lens went on rendering whatever file the Git panel had last opened, and
|
|
285
|
+
switching the source tab changed nothing at all. */
|
|
286
|
+
const path = source.documentPath;
|
|
287
|
+
if (!gitChangeFor(path)) {
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
reason: "Nothing to compare — this file matches HEAD.",
|
|
291
|
+
status: "unavailable",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (derived.diff?.filePath === path) {
|
|
295
|
+
return { ...base, reason: "", status: "ready" };
|
|
296
|
+
}
|
|
297
|
+
/* A read that came back EMPTY is an answer, and it has to survive the next frame. The failure
|
|
298
|
+
used to be written straight onto `derived.status`, where the follow's very next resolve
|
|
299
|
+
overwrote it with `loading` — so a lens whose comparison could not be read said "Loading
|
|
300
|
+
this file's changes…" for the rest of the session while asking for nothing. `_diffLoads`
|
|
301
|
+
remembers per pane which path was asked and how it went, and the answer is composed here
|
|
302
|
+
with every other one. */
|
|
303
|
+
if (_diffLoads.get(paneId)?.failed && _diffLoads.get(paneId)?.path === path) {
|
|
304
|
+
return { ...base, reason: DIFF_UNREADABLE, status: "unavailable" };
|
|
305
|
+
}
|
|
306
|
+
return { ...base, diffPath: path, reason: "", status: "loading" };
|
|
307
|
+
}
|
|
308
|
+
if (derived.preset === "code" && canvasModeOfTab(source) === "source") {
|
|
309
|
+
/* TWO MONACO MODELS ON ONE URI, refused for as long as the state lasts rather than once at
|
|
310
|
+
creation. `presetRefusal` decides whether a Code lens can be MADE; nothing decided whether
|
|
311
|
+
it could still be drawn, and one click on the source pane's Editor axis put both panes in
|
|
312
|
+
Code over the same file. Real Monaco throws `Cannot create model because a model with the
|
|
313
|
+
same URI already exists` inside the floating `void mountSourceEditor(…)` in
|
|
314
|
+
`canvas/canvas-render.ts` — an unhandled rejection and a blank Code pane. Unreachable before
|
|
315
|
+
derived panes, because one pane could show one document once. */
|
|
316
|
+
return {
|
|
317
|
+
...base,
|
|
318
|
+
reason: "The pane this one follows is showing Code — one document has one editor.",
|
|
319
|
+
status: "unavailable",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (derived.preset === "breakpoint" && !declaredMedia(source).includes(derived.media)) {
|
|
323
|
+
return {
|
|
324
|
+
...base,
|
|
325
|
+
reason: `This document does not declare a "${derived.media ?? "base"}" breakpoint.`,
|
|
326
|
+
status: "unavailable",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return { ...base, reason: "", status: "ready" };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The source-control entry for `path`, when it has changes a comparison can be built from.
|
|
334
|
+
*
|
|
335
|
+
* `M` and `A` and nothing else — the same two `panels/git-panel.ts` opens a diff for, because an
|
|
336
|
+
* untracked or deleted file has no pair of texts to put side by side. `shell.git.status` is the
|
|
337
|
+
* project's whole answer to "what has changed", so a diff lens can be offered for a page the author
|
|
338
|
+
* has never clicked in the Git panel, which is the common case and was the refused one.
|
|
339
|
+
*
|
|
340
|
+
* A `null` path needs no guard of its own: no entry in `files` has a null path, so the search
|
|
341
|
+
* simply finds nothing, and a guard whose true branch returns what the false branch already returns
|
|
342
|
+
* is a branch no test can distinguish.
|
|
343
|
+
*
|
|
344
|
+
* @param {string | null} path
|
|
345
|
+
* @returns {GitFileStatus | null}
|
|
346
|
+
*/
|
|
347
|
+
function gitChangeFor(path: string | null): GitFileStatus | null {
|
|
348
|
+
return (
|
|
349
|
+
shell.git.status?.files.find(
|
|
350
|
+
(file) => file.path === path && (file.status === "M" || file.status === "A"),
|
|
351
|
+
) ?? null
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A companion's target: the path the rule resolves to, or `null` to HOLD.
|
|
357
|
+
*
|
|
358
|
+
* **`null` is a hold, not a change**, and that is the difference between a usable following pane
|
|
359
|
+
* and one nobody leaves open. Clicking a paragraph outside any component resolves to no definition;
|
|
360
|
+
* blanking the pane on that click makes it flicker between a document and an empty state as the
|
|
361
|
+
* author works, and this is the preset somebody leaves open all day.
|
|
362
|
+
*
|
|
363
|
+
* **A hold is only honest where the next gesture could answer differently.** `component` holds
|
|
364
|
+
* because the author's next click may land inside one. `locale` does not: a document with no copy
|
|
365
|
+
* in that language has no copy in it until somebody writes one, and a pane silently holding the
|
|
366
|
+
* previous document under a chip saying "Same page in français" is a lie the author cannot see
|
|
367
|
+
* through. That answer is `unavailable` with the sentence, which is what makes creating the
|
|
368
|
+
* translation the obvious next move rather than a guess.
|
|
369
|
+
*/
|
|
370
|
+
function companionTarget(
|
|
371
|
+
paneId: string,
|
|
372
|
+
derived: Extract<PaneDerivation, { kind: "companion" }>,
|
|
373
|
+
source: Tab,
|
|
374
|
+
): DerivedTarget {
|
|
375
|
+
const base = { diffPath: null, kind: "companion" as const, media: null, mode: null };
|
|
376
|
+
if (derived.preset === "layout") {
|
|
377
|
+
/* THE FILE THE CLICKED CHROME CAME FROM, when there is one.
|
|
378
|
+
`shell.layoutSelection` is a `LayoutHit`: the layout document the canvas hit test resolved
|
|
379
|
+
and the node's path INSIDE it. Resolving the page's own `$layout` instead is a different
|
|
380
|
+
answer for a nested chain — click a header contributed by `layouts/marketing.json`, which
|
|
381
|
+
`layouts/base.json` wraps, and the page's `$layout` names the wrong one of the two. It is
|
|
382
|
+
also the only thing that carries the SELECTION, which is what "Open Layout →" is for. The
|
|
383
|
+
hit is transient — `iframe-host.ts` clears it the moment the author clicks page content —
|
|
384
|
+
so the page's own layout remains the answer for a derivation created from the preset menu. */
|
|
385
|
+
const hit = shell.layoutSelection;
|
|
386
|
+
const path =
|
|
387
|
+
hit?.layoutFile ??
|
|
388
|
+
getEffectiveLayoutPath(source.doc.document.$layout as string | false | undefined);
|
|
389
|
+
return path === null
|
|
390
|
+
? {
|
|
391
|
+
...base,
|
|
392
|
+
path: null,
|
|
393
|
+
probePath: null,
|
|
394
|
+
reason: "This page has no layout.",
|
|
395
|
+
select: null,
|
|
396
|
+
status: "unavailable",
|
|
397
|
+
}
|
|
398
|
+
: {
|
|
399
|
+
...base,
|
|
400
|
+
path,
|
|
401
|
+
probePath: null,
|
|
402
|
+
reason: "",
|
|
403
|
+
/* A SELECTION only when a click produced one. `path` IS `hit.layoutFile` whenever there
|
|
404
|
+
is a hit — the line above says so — so a second `hit.layoutFile === path` guard here
|
|
405
|
+
was a condition with no false branch, and an unreachable branch is a claim nobody can
|
|
406
|
+
check. What stops a node path from layout A landing inside layout B is
|
|
407
|
+
{@link selectInPane}, which compares against the document actually on screen. */
|
|
408
|
+
select: hit ? (hit.layoutPath as JxPath) : null,
|
|
409
|
+
status: "ready",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (derived.preset === "locale") {
|
|
413
|
+
/* WHERE THE TRANSLATION WOULD LIVE, which is a different question from whether it is there.
|
|
414
|
+
`translationPathFor` is the same string math the build runs, so the path this resolves to is
|
|
415
|
+
the path the site would serve; it answers null for a document no locale can address at all —
|
|
416
|
+
a layout, a component, a file at the project root — and for a tag the project stopped
|
|
417
|
+
declaring, which is the case {@link presetRefusal} cannot catch because it runs once. */
|
|
418
|
+
const i18n = getEffectiveLocales();
|
|
419
|
+
const wanted =
|
|
420
|
+
source.documentPath === null || derived.locale === null
|
|
421
|
+
? null
|
|
422
|
+
: translationPathFor(source.documentPath, derived.locale, i18n);
|
|
423
|
+
if (wanted === null) {
|
|
424
|
+
return {
|
|
425
|
+
...base,
|
|
426
|
+
path: null,
|
|
427
|
+
probePath: null,
|
|
428
|
+
reason: "This document has no path in that locale.",
|
|
429
|
+
select: null,
|
|
430
|
+
status: "unavailable",
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/* AND WHETHER IT IS THERE, which this function cannot ask: it is pure and synchronous, and
|
|
434
|
+
handing back a path for a file that does not exist makes `openFileInPane` fail into a blank
|
|
435
|
+
pane — §18.4's last paragraph refuses exactly that. {@link _localeProbes} is the same shape
|
|
436
|
+
{@link _diffLoads} is, for the same reason: a terminal answer written onto `derived.status`
|
|
437
|
+
is overwritten by the next frame's resolve, so the pane would say "Loading…" forever about a
|
|
438
|
+
file nobody is reading. */
|
|
439
|
+
const probe = _localeProbes.get(paneId);
|
|
440
|
+
if (probe?.path !== wanted || probe.exists === null) {
|
|
441
|
+
return {
|
|
442
|
+
...base,
|
|
443
|
+
path: null,
|
|
444
|
+
probePath: wanted,
|
|
445
|
+
reason: "",
|
|
446
|
+
select: null,
|
|
447
|
+
status: "loading",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (!probe.exists) {
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
path: null,
|
|
454
|
+
probePath: null,
|
|
455
|
+
reason:
|
|
456
|
+
`No ${localeLabel(derived.locale)} copy of this document yet — ` +
|
|
457
|
+
"the Languages panel can create one.",
|
|
458
|
+
select: null,
|
|
459
|
+
status: "unavailable",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return { ...base, path: wanted, probePath: null, reason: "", select: null, status: "ready" };
|
|
463
|
+
}
|
|
464
|
+
const path = componentPathUnderSelection(source);
|
|
465
|
+
if (path === null) {
|
|
466
|
+
return derived.resolved === null
|
|
467
|
+
? {
|
|
468
|
+
...base,
|
|
469
|
+
path: null,
|
|
470
|
+
probePath: null,
|
|
471
|
+
reason: "Select an element inside a component to see its definition.",
|
|
472
|
+
select: null,
|
|
473
|
+
status: "unavailable",
|
|
474
|
+
}
|
|
475
|
+
: { ...base, path: null, probePath: null, reason: "", select: null, status: "ready" };
|
|
476
|
+
}
|
|
477
|
+
return { ...base, path, probePath: null, reason: "", select: null, status: "ready" };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Every size breakpoint a document declares, plus `null` for base. */
|
|
481
|
+
export function declaredMedia(tab: Tab): (string | null)[] {
|
|
482
|
+
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
|
|
483
|
+
return [null, ...sizeBreakpoints.map((bp) => bp.name)];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* There is no `rawDocOf`. It read the document through `toRaw` under a docstring explaining that
|
|
487
|
+
traversing the reactive proxy would make every node the walk touches a dependency — true of a
|
|
488
|
+
read INSIDE an effect, and none of these are. **Tracking is a property of where a read happens,
|
|
489
|
+
not of which object it happens on**: {@link derivedTarget} and {@link presetRefusal} are reached
|
|
490
|
+
from `queueRetarget`'s rAF, from `loadDiffFor`'s `.then`, from a command's `run` and from
|
|
491
|
+
`panels/pane-context.ts`'s click handler, and there is no active effect at any of those. So the
|
|
492
|
+
`toRaw` bought nothing that being outside an effect had not already bought, and no state the app
|
|
493
|
+
can reach could tell it from its absence. What the resolvers DO read is declared by
|
|
494
|
+
{@link installDerivationEffects}, which is the only place a read of theirs is ever observed. */
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The component definition the selection sits inside, or null.
|
|
498
|
+
*
|
|
499
|
+
* Walks UP from the selected node, because the author clicks the paragraph and means the card. That
|
|
500
|
+
* is also what makes the memo work: twenty clicks inside one `<my-card>` all resolve to the same
|
|
501
|
+
* definition, so `applyDerivation` does nothing at all — no render, no host post, no file read.
|
|
502
|
+
*
|
|
503
|
+
* @param {Tab | null} tab
|
|
504
|
+
* @returns {string | null}
|
|
505
|
+
*/
|
|
506
|
+
export function componentPathUnderSelection(tab: Tab | null): string | null {
|
|
507
|
+
const [selected] = tab?.session.selection ?? [];
|
|
508
|
+
if (!tab || !selected) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const doc = tab.doc.document;
|
|
512
|
+
let path: JxPath | null = [...selected];
|
|
513
|
+
while (path) {
|
|
514
|
+
const node = getNodeAtPath(doc, path);
|
|
515
|
+
const entry = node
|
|
516
|
+
? componentRegistry.find((candidate) => candidate.tagName === node.tagName)
|
|
517
|
+
: null;
|
|
518
|
+
if (entry?.path) {
|
|
519
|
+
return entry.path;
|
|
520
|
+
}
|
|
521
|
+
path = parentElementPath(path) as JxPath | null;
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── Writing ─────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Make `paneId` a derived pane. The one writer of `Pane.derived`, with {@link clearPaneDerivation}.
|
|
530
|
+
*
|
|
531
|
+
* Refuses a derivation naming a pane that is not in the grid, or naming this pane — invariant D1,
|
|
532
|
+
* asserted and never repaired: a self-derivation is an infinite `tabOfPane` hop, and a dangling
|
|
533
|
+
* `sourcePaneId` is a stage drawing a document that has no pane.
|
|
534
|
+
*
|
|
535
|
+
* @param {string} paneId
|
|
536
|
+
* @param {PaneDerivation} derivation
|
|
537
|
+
*/
|
|
538
|
+
export function setPaneDerivation(paneId: string, derivation: PaneDerivation): void {
|
|
539
|
+
if (derivation.sourcePaneId === paneId || !paneById(derivation.sourcePaneId)) {
|
|
540
|
+
throw new RangeError(
|
|
541
|
+
`pane "${paneId}" cannot derive from "${derivation.sourcePaneId}" — a derivation names ` +
|
|
542
|
+
`another pane that is in the grid`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
writeDerivation(paneId, derivation);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Write `pane.derived` and forget the answers the previous one had in hand — its comparison and its
|
|
550
|
+
* translation probe alike. **The only writer, both ways.**
|
|
551
|
+
*
|
|
552
|
+
* A new derivation is a new question, so the previous one's answer is not an answer to it — and a
|
|
553
|
+
* pane that has stopped deriving has no question at all. That was two `_diffLoads.delete` calls
|
|
554
|
+
* under a comment reading "both writers forget it; nothing else may", which is an invariant stated
|
|
555
|
+
* twice and enforced in neither place: only one of the two could be reached by a test, so inverting
|
|
556
|
+
* the other was a mutation the whole suite could not see. One line, one call site each.
|
|
557
|
+
*
|
|
558
|
+
* `workspace.ts` writes `pane.derived = null` twice more — {@link receivingPane} and `closePane`'s
|
|
559
|
+
* D4 — and deliberately does not route through here: those are grid facts, this module imports from
|
|
560
|
+
* that one, and a pane can only derive AGAIN through {@link setPaneDerivation}, which forgets.
|
|
561
|
+
*/
|
|
562
|
+
function writeDerivation(paneId: string, derivation: PaneDerivation | null): void {
|
|
563
|
+
const pane = paneById(paneId);
|
|
564
|
+
if (pane) {
|
|
565
|
+
_diffLoads.delete(paneId);
|
|
566
|
+
_localeProbes.delete(paneId);
|
|
567
|
+
pane.derived = derivation;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Stop deriving. The pane keeps whatever tabs it owns — a companion's document stays open, which is
|
|
573
|
+
* exactly what Pin means — and a lens, owning none, becomes an empty pane its next collapse
|
|
574
|
+
* removes.
|
|
575
|
+
*
|
|
576
|
+
* @param {string} paneId
|
|
577
|
+
*/
|
|
578
|
+
export function clearPaneDerivation(paneId: string): void {
|
|
579
|
+
writeDerivation(paneId, null);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Bring `paneId` into line with {@link derivedTarget}. Idempotent, and safe to run every frame.
|
|
584
|
+
*
|
|
585
|
+
* The only impure half of the follow, and it does exactly two things: it records the status the
|
|
586
|
+
* pane draws, and — for a companion whose answer has CHANGED — it re-points the pane at the new
|
|
587
|
+
* document. The memo is on `resolved`, the answer, not on the selection that produced it.
|
|
588
|
+
*
|
|
589
|
+
* @param {string} paneId
|
|
590
|
+
* @param {DerivationDeps} deps
|
|
591
|
+
*/
|
|
592
|
+
export function applyDerivation(paneId: string, deps: DerivationDeps): void {
|
|
593
|
+
const pane = paneById(paneId);
|
|
594
|
+
const derived = pane?.derived;
|
|
595
|
+
const target = derivedTarget(paneId);
|
|
596
|
+
if (!pane || !derived || !target) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
canvasPerf.derivedResolves += 1;
|
|
600
|
+
derived.status = target.status;
|
|
601
|
+
derived.reason = target.reason;
|
|
602
|
+
if (derived.kind === "lens") {
|
|
603
|
+
/* Invariant D2, enforced rather than assumed: a lens owns NO tab. Anything sitting in its
|
|
604
|
+
strip when the lens was created has already been handed back to the source pane by
|
|
605
|
+
`pane.derive`; this is the belt to those braces, and it is what keeps `paneOfTab` — which
|
|
606
|
+
every destructive tab verb resolves through — unable to name a lens pane at all.
|
|
607
|
+
The tabs MOVE rather than being dropped: a detach with no insert would leave a live tab in
|
|
608
|
+
`workspace.tabs` that no pane holds, which is a document nobody can reach and nobody can
|
|
609
|
+
close. Nothing a derivation does may lose a document. */
|
|
610
|
+
const source = paneById(derived.sourcePaneId);
|
|
611
|
+
if (pane.tabOrder.length > 0 || pane.activeTabId !== null) {
|
|
612
|
+
const owned = [...pane.tabOrder];
|
|
613
|
+
for (const tabId of owned) {
|
|
614
|
+
detachTab(tabId);
|
|
615
|
+
if (source) {
|
|
616
|
+
insertIntoPane(source, tabId);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
pane.activeTabId = null;
|
|
620
|
+
}
|
|
621
|
+
/* A DIFF LENS HOLDS A COMPARISON ONLY WHILE IT IS THE SOURCE DOCUMENT'S, and nothing enforced
|
|
622
|
+
that. `derivedTarget` says `ready` for exactly the case where the one in hand is the right
|
|
623
|
+
one; every other answer — the wrong file in hand after a tab switch, a source document with
|
|
624
|
+
no changes, no source document at all — means the pane is holding somebody else's, and
|
|
625
|
+
`canvas/canvas-render.ts` draws whatever is in this field rather than the notice beside it.
|
|
626
|
+
Clearing it is what makes that file's `if (!gitDiffState)` fire. The previous release
|
|
627
|
+
shipped a comment claiming this was fixed and no line that did it. */
|
|
628
|
+
if (derived.preset === "diff" && target.status !== "ready") {
|
|
629
|
+
derived.diff = null;
|
|
630
|
+
}
|
|
631
|
+
loadDiffFor(paneId, target, deps);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
/* FIRST, unlike {@link loadDiffFor}, which is the last thing the lens half does. A probe that has
|
|
635
|
+
not answered yet resolves to `path: null` — the hold — and the hold returns two statements
|
|
636
|
+
below, so a call at the end of this function would never be reached by the one target that
|
|
637
|
+
needs it. */
|
|
638
|
+
probeTranslationFor(paneId, target, deps);
|
|
639
|
+
/* THE MEMO IS ABOUT WHAT IS ON SCREEN, so it cannot outlive the document being on screen.
|
|
640
|
+
A resolved companion owns a real tab, so `panels/tab-strip.ts` draws a real chip with a real
|
|
641
|
+
✕ — and closing it left the pane holding a derivation whose `resolved` still named the file
|
|
642
|
+
that had just gone. `paneIsEmpty` counts the derivation as a subject so the pane stayed; the
|
|
643
|
+
strip fell back to the derivation chip; and the stage said "Looking for something to show
|
|
644
|
+
here…" for the rest of the session, because every following frame found `target.path ===
|
|
645
|
+
derived.resolved` and did nothing. Forgetting the answer when the pane is showing nothing is
|
|
646
|
+
what lets the next frame ask the question again. */
|
|
647
|
+
if (derived.resolved !== null && tabOfPane(paneId) === null) {
|
|
648
|
+
derived.resolved = null;
|
|
649
|
+
}
|
|
650
|
+
if (target.path === null || target.path === derived.resolved) {
|
|
651
|
+
// Same document, possibly a different node inside it — clicking a second piece of layout chrome
|
|
652
|
+
// Moves the selection without re-reading the file.
|
|
653
|
+
selectInPane(paneId, target);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
derived.resolved = target.path;
|
|
657
|
+
canvasPerf.derivedRetargets += 1;
|
|
658
|
+
const opening = deps.openFileInPane(paneId, target.path);
|
|
659
|
+
if (target.select === null) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
/* The selection lands AFTER the open, because there is no tab to select in until then. Awaiting
|
|
663
|
+
rather than firing both is what makes "Open Layout →" arrive at the node the author clicked
|
|
664
|
+
instead of at the top of a file they now have to search. */
|
|
665
|
+
void Promise.resolve(opening).then(() => {
|
|
666
|
+
selectInPane(paneId, target);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Select `target.select` in whatever `paneId` is showing, when that is the document it is about.
|
|
672
|
+
*
|
|
673
|
+
* Idempotent, and it has to be: {@link applyDerivation} runs every frame the follow fires, and a
|
|
674
|
+
* `session.selection` write is a reactive write that repaints the Inspector, the overlays and the
|
|
675
|
+
* block bar. Comparing first is what keeps a following pane free.
|
|
676
|
+
*/
|
|
677
|
+
function selectInPane(paneId: string, target: DerivedTarget): void {
|
|
678
|
+
const wanted = target.select;
|
|
679
|
+
const tab = tabOfPane(paneId);
|
|
680
|
+
if (!wanted || !tab || (target.path !== null && tab.documentPath !== target.path)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const [current] = tab.session.selection;
|
|
684
|
+
if (current?.length === wanted.length && current.every((seg, i) => seg === wanted[i])) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
tab.session.selection = [[...wanted]];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The comparison each diff lens has ASKED for, and how that went.
|
|
692
|
+
*
|
|
693
|
+
* Never cleared on COMPLETION, only when the wanted path changes, so the follow's next frame cannot
|
|
694
|
+
* re-issue a failing request forever. `failed` is the half that was missing: the status
|
|
695
|
+
* `loadDiffFor` wrote when the read came back empty was overwritten by the very next resolve, and
|
|
696
|
+
* the pane went back to saying "Loading this file's changes…" permanently. A terminal answer has to
|
|
697
|
+
* live where {@link lensTarget} — which composes every other answer — can see it.
|
|
698
|
+
*/
|
|
699
|
+
const _diffLoads = new Map<string, { path: string; failed: boolean }>();
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Fetch the comparison a diff lens is missing, once.
|
|
703
|
+
*
|
|
704
|
+
* @param {string} paneId
|
|
705
|
+
* @param {DerivedTarget} target
|
|
706
|
+
* @param {DerivationDeps} deps
|
|
707
|
+
*/
|
|
708
|
+
function loadDiffFor(paneId: string, target: DerivedTarget, deps: DerivationDeps): void {
|
|
709
|
+
const path = target.diffPath;
|
|
710
|
+
const change = gitChangeFor(path);
|
|
711
|
+
if (path === null || !change || _diffLoads.get(paneId)?.path === path) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
_diffLoads.set(paneId, { failed: false, path });
|
|
715
|
+
deps
|
|
716
|
+
.loadDiff(path, change.status)
|
|
717
|
+
.then((state) => {
|
|
718
|
+
const live = paneById(paneId)?.derived;
|
|
719
|
+
if (
|
|
720
|
+
live?.kind !== "lens" ||
|
|
721
|
+
live.preset !== "diff" ||
|
|
722
|
+
_diffLoads.get(paneId)?.path !== path
|
|
723
|
+
) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (state) {
|
|
727
|
+
live.diff = state;
|
|
728
|
+
} else {
|
|
729
|
+
_diffLoads.set(paneId, { failed: true, path });
|
|
730
|
+
}
|
|
731
|
+
/* Re-resolve rather than writing a status here: `ready` with the comparison in hand,
|
|
732
|
+
`unavailable` with {@link DIFF_UNREADABLE} when the read came back empty — composed in the
|
|
733
|
+
one function that composes every other answer, so the next frame agrees with this one. It
|
|
734
|
+
cannot re-enter, because neither of those answers carries a `diffPath`. */
|
|
735
|
+
applyDerivation(paneId, deps);
|
|
736
|
+
})
|
|
737
|
+
.catch((error: unknown) => {
|
|
738
|
+
console.error("loadDiff error:", error);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Which translation each locale companion has ASKED about, and whether it is there.
|
|
744
|
+
*
|
|
745
|
+
* {@link _diffLoads}'s twin, and cleared by the same writer for the same reason. `exists` is
|
|
746
|
+
* THREE-valued on purpose: `null` is "asked, no answer yet", which is the state the pane draws
|
|
747
|
+
* "Loading…" for, and it cannot be folded into `false` — `false` is a terminal answer with a
|
|
748
|
+
* sentence naming the language and offering to create the file, and showing that sentence for the
|
|
749
|
+
* frame between the question and the answer would tell the author their translation is missing
|
|
750
|
+
* every time they open the pane.
|
|
751
|
+
*/
|
|
752
|
+
const _localeProbes = new Map<string, { path: string; exists: boolean | null }>();
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Ask whether the translation a locale companion wants is on disk, once per wanted path.
|
|
756
|
+
*
|
|
757
|
+
* @param {string} paneId
|
|
758
|
+
* @param {DerivedTarget} target
|
|
759
|
+
* @param {DerivationDeps} deps
|
|
760
|
+
*/
|
|
761
|
+
function probeTranslationFor(paneId: string, target: DerivedTarget, deps: DerivationDeps): void {
|
|
762
|
+
const path = target.probePath;
|
|
763
|
+
if (path === null || _localeProbes.get(paneId)?.path === path) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
_localeProbes.set(paneId, { exists: null, path });
|
|
767
|
+
deps
|
|
768
|
+
.fileExists(path)
|
|
769
|
+
.then((exists) => {
|
|
770
|
+
const live = paneById(paneId)?.derived;
|
|
771
|
+
/* THE ANSWER TO THE QUESTION THAT IS STILL BEING ASKED. A read issued for `fr` that lands
|
|
772
|
+
after the author retargeted the pane at `de` would otherwise write `de`'s memo with `fr`'s
|
|
773
|
+
answer — the same late-landing hazard {@link loadDiffFor} carries, and the same guard. */
|
|
774
|
+
if (
|
|
775
|
+
live?.kind !== "companion" ||
|
|
776
|
+
live.preset !== "locale" ||
|
|
777
|
+
_localeProbes.get(paneId)?.path !== path
|
|
778
|
+
) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
_localeProbes.set(paneId, { exists, path });
|
|
782
|
+
/* Re-resolve rather than writing a status here, for {@link loadDiffFor}'s reason: both
|
|
783
|
+
answers are composed by {@link companionTarget} beside every other one, and neither of
|
|
784
|
+
them carries a `probePath`, so this cannot re-enter. */
|
|
785
|
+
applyDerivation(paneId, deps);
|
|
786
|
+
})
|
|
787
|
+
.catch((error: unknown) => {
|
|
788
|
+
console.error("fileExists error:", error);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ─── The follow ───────────────────────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
/** One pending retarget per pane. A shared id would let one pane swallow the other's frame. */
|
|
795
|
+
const _retargetRafIds = new Map<string, number>();
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Run `applyDerivation` on the next frame, once per pane however many times it is asked.
|
|
799
|
+
*
|
|
800
|
+
* **Scheduled, never synchronous.** The effect below reads `workspace.panes`, and a retarget writes
|
|
801
|
+
* `pane.tabOrder` through `openFileInPane` — an effect that triggers itself, which is the hazard
|
|
802
|
+
* `panels/pane-grid.ts` names at its own reconciler. The rAF hop is the same shape
|
|
803
|
+
* `scheduleCanvasRender` uses, for the same reason.
|
|
804
|
+
*/
|
|
805
|
+
function queueRetarget(paneId: string, deps: DerivationDeps): void {
|
|
806
|
+
if (_retargetRafIds.has(paneId)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
_retargetRafIds.set(
|
|
810
|
+
paneId,
|
|
811
|
+
requestAnimationFrame(() => {
|
|
812
|
+
_retargetRafIds.delete(paneId);
|
|
813
|
+
try {
|
|
814
|
+
applyDerivation(paneId, deps);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.error("applyDerivation error:", error);
|
|
817
|
+
}
|
|
818
|
+
}),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Subscribe ONE pane's follow. Called from `studio.ts`'s per-pane effect scope, so it is stopped
|
|
824
|
+
* when the pane leaves the grid.
|
|
825
|
+
*
|
|
826
|
+
* **Most of the follow is STRUCTURAL, and that is still the design.** `tabOfPane(paneId)` reads the
|
|
827
|
+
* source pane's `activeTabId`, so the render effects already installed for this pane track it and
|
|
828
|
+
* repaint when the source pane switches document.
|
|
829
|
+
*
|
|
830
|
+
* **Every derivation subscribes, and this effect is where.** No preset "follows structurally and
|
|
831
|
+
* declares nothing": the body below reads THIS pane's `activeTabId` and `tabOfPane(sourcePaneId)`
|
|
832
|
+
* unconditionally, whatever the preset — `diff` and `breakpoint` are the two that add nothing on
|
|
833
|
+
* top. An input a resolver consults and the effect does not observe is an answer nobody asks for:
|
|
834
|
+
* this docstring claimed the `$layout` subscription for a release while the effect did not have it,
|
|
835
|
+
* the Code lens's refusal against a source pane showing Code arrived the same way, and so did
|
|
836
|
+
* {@link applyDerivation}'s memo-forget, whose comment described closing a companion's document
|
|
837
|
+
* while nothing re-ran the follow when it closed.
|
|
838
|
+
*
|
|
839
|
+
* The list is BY INPUT rather than by preset, because `layout` declares two of them. Every input is
|
|
840
|
+
* a HUMAN GESTURE rather than a keystroke — closing a tab in this pane, switching the source pane's
|
|
841
|
+
* document, a layout click, a selection, a click on the Editor axis — except the page's own
|
|
842
|
+
* `$layout`, whose per-transaction cost is stated at the read itself. `layout` follows the layout
|
|
843
|
+
* HIT and `$layout`; `component` is the only preset that observes the SELECTION, and it memoises on
|
|
844
|
+
* its answer; `code` observes the source pane's canvas mode, because the refusal that keeps two
|
|
845
|
+
* Monaco models off one URI is a standing answer about a value one click changes.
|
|
846
|
+
*
|
|
847
|
+
* Each of those claims is a test rather than a sentence — see `pane-derive.test.ts`'s "the follow
|
|
848
|
+
* NOTICES the close", "a companion re-resolves when the SOURCE pane switches document", "the layout
|
|
849
|
+
* follow OBSERVES the hit", "changing the page's layout MOVES the companion", "clicking into a
|
|
850
|
+
* component RE-POINTS the pane" and "a Code lens notices the source pane switching to Code".
|
|
851
|
+
*
|
|
852
|
+
* @param {string} paneId
|
|
853
|
+
* @param {DerivationDeps} deps
|
|
854
|
+
*/
|
|
855
|
+
export function installDerivationEffects(paneId: string, deps: DerivationDeps): void {
|
|
856
|
+
effect(() => {
|
|
857
|
+
const pane = paneById(paneId);
|
|
858
|
+
const derived = pane?.derived;
|
|
859
|
+
if (!derived) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
/* THIS PANE'S OWN TAB, for both kinds, and its absence is the interesting value.
|
|
863
|
+
`applyDerivation` forgets a companion's memo when the pane is showing nothing, so the next
|
|
864
|
+
frame asks the question again — and there was no next frame. The tracked inputs were the
|
|
865
|
+
SOURCE pane's tab and the selection, neither of which a `closeTab` in the following pane
|
|
866
|
+
touches, so closing the document a layout companion had opened left the pane stranded behind
|
|
867
|
+
a memo naming a file that was gone: the exact failure that comment describes, with the fix
|
|
868
|
+
one call away from ever running. A lens reads it too, for one line's worth of the same
|
|
869
|
+
reason: invariant D2 says a lens owns no tab, `applyDerivation` hands one back if it finds
|
|
870
|
+
one, and an insertion nothing observes is an insertion nothing repairs.
|
|
871
|
+
It costs one resolve per open or close in this pane — a gesture, not a keystroke — and no
|
|
872
|
+
retarget, because the answer the memo compares against has not changed. */
|
|
873
|
+
void pane.activeTabId;
|
|
874
|
+
/* The rest of the tracked inputs, and only these: which tab the source pane is showing, and —
|
|
875
|
+
for the presets that observe them — that tab's selection, editor kind and `$layout`. What
|
|
876
|
+
the RESOLVERS read is not among them, because they run in `queueRetarget`'s rAF where no
|
|
877
|
+
effect is active: an untracked read is untracked because of WHERE it happens. So typing,
|
|
878
|
+
which replaces `tab.doc.document`, re-runs this effect only for the one preset that reads
|
|
879
|
+
that field HERE. */
|
|
880
|
+
const source = tabOfPane(derived.sourcePaneId);
|
|
881
|
+
if (derived.kind === "lens" && derived.preset === "code") {
|
|
882
|
+
/* The SOURCE pane's editor kind, and only this preset reads it. {@link lensTarget} refuses a
|
|
883
|
+
Code lens for as long as the pane it follows is ALSO showing Code — two Monaco models on
|
|
884
|
+
one URI — and that is a standing answer about a value one click on the Editor axis
|
|
885
|
+
changes. Untracked, the refusal would be computed only on some later frame that
|
|
886
|
+
re-resolved for another reason, and until then the stage mounts the second model, which is
|
|
887
|
+
the throw the refusal exists to prevent. One human gesture per change, like the layout
|
|
888
|
+
hit; nothing per keystroke. */
|
|
889
|
+
void source?.session.ui.canvasMode;
|
|
890
|
+
}
|
|
891
|
+
if (derived.kind === "companion" && derived.preset === "component") {
|
|
892
|
+
void source?.session.selection;
|
|
893
|
+
}
|
|
894
|
+
/* `locale` ADDS NOTHING, and the omission is the honest answer rather than a gap. The two
|
|
895
|
+
inputs above — this pane's own tab and the source pane's — are exactly what it reads: the
|
|
896
|
+
locale is fixed on the derivation, and the file it wants is a function of the source
|
|
897
|
+
document's path. Its third input, the project's declared locales, CANNOT be tracked:
|
|
898
|
+
`projectState` is a plain `let` (`store/state.ts`) that `setProjectState` replaces whole, so
|
|
899
|
+
no effect anywhere observes it. Adding a locale in Project Settings therefore does not
|
|
900
|
+
re-run this follow; the next retarget — any tab switch in either pane — picks it up, and
|
|
901
|
+
{@link companionTarget} re-reads the list every time it resolves, so the pane cannot go on
|
|
902
|
+
showing a locale the project has stopped declaring. A `void` read of a non-reactive value
|
|
903
|
+
would look like a subscription and be none, which is the shape the `layout` preset shipped
|
|
904
|
+
a release of. */
|
|
905
|
+
if (derived.kind === "companion" && derived.preset === "layout") {
|
|
906
|
+
/* The LAYOUT hit, which is what "Open Layout →" is a follow of. It changes only when the
|
|
907
|
+
author clicks layout chrome (`iframe-host.ts` writes it) or clicks page content (which
|
|
908
|
+
clears it) — a human gesture, not a keystroke — so it costs nothing to observe and it is
|
|
909
|
+
the difference between a following layout pane and one frozen on the first header. */
|
|
910
|
+
void shell.layoutSelection;
|
|
911
|
+
/* AND the page's own `$layout`, which is the answer whenever there is no hit. The docstring
|
|
912
|
+
above claimed this preset followed it while the effect did not read it at all: the
|
|
913
|
+
resolver's read happens in the rAF, where nothing is tracking, so changing a page's layout
|
|
914
|
+
in the Inspector left the companion on the old file with the pure half answering correctly
|
|
915
|
+
and nothing calling it. An input a resolver consults and the effect does not observe is an
|
|
916
|
+
answer nobody asks for.
|
|
917
|
+
The cost is stated rather than hidden. `tab.doc.document` is replaced by every
|
|
918
|
+
transaction, so this re-runs on a keystroke — but `queueRetarget` collapses a frame's
|
|
919
|
+
worth into one rAF and `applyDerivation` memoises on the resolved PATH, so the expensive
|
|
920
|
+
half (the file read) still happens exactly once per real change of layout. `component`
|
|
921
|
+
does NOT read it, and its memo test is what holds that: it types twenty characters and
|
|
922
|
+
asserts the resolve count does not move, which a per-transaction read here would break. */
|
|
923
|
+
void source?.doc.document?.$layout;
|
|
924
|
+
}
|
|
925
|
+
queueRetarget(paneId, deps);
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* The refusal for a preset that cannot be built right now, or null when it can.
|
|
933
|
+
*
|
|
934
|
+
* **Run-time, not `enablement`** — the same shape `canvas.setMode` uses, and for the same reason:
|
|
935
|
+
* `enablement` is ONE sentence for the whole command and five presets have five reasons. The menu
|
|
936
|
+
* renders each row disabled with the sentence this returns, and `run` throws a `RangeError`
|
|
937
|
+
* carrying it, so the tooltip and the refusal are the same words.
|
|
938
|
+
*
|
|
939
|
+
* `locale` takes a fourth argument, DEFAULTED rather than added to every call: the two callers that
|
|
940
|
+
* ask about a breakpoint have nothing to say about a language, and a required parameter would have
|
|
941
|
+
* made them pass a `null` that means nothing at those sites.
|
|
942
|
+
*
|
|
943
|
+
* @param {DerivePreset} preset
|
|
944
|
+
* @param {string} sourcePaneId
|
|
945
|
+
* @param {string | null} media
|
|
946
|
+
* @param {string | null} locale
|
|
947
|
+
* @returns {string | null}
|
|
948
|
+
*/
|
|
949
|
+
export function presetRefusal(
|
|
950
|
+
preset: DerivePreset,
|
|
951
|
+
sourcePaneId: string,
|
|
952
|
+
media: string | null,
|
|
953
|
+
locale: string | null = null,
|
|
954
|
+
): string | null {
|
|
955
|
+
const source = tabOfPane(sourcePaneId);
|
|
956
|
+
if (!source) {
|
|
957
|
+
return "an open document";
|
|
958
|
+
}
|
|
959
|
+
/* WHAT THE DOCUMENT CAN BE SHOWN AS, asked before anything else. See {@link MODE_FOR_PRESET}:
|
|
960
|
+
three of the five presets put the pane into a named canvas mode, and the menu offered all five
|
|
961
|
+
over a document whose format declares neither. */
|
|
962
|
+
const needed = MODE_FOR_PRESET[preset];
|
|
963
|
+
if (needed && !source.capabilities.modes.includes(needed)) {
|
|
964
|
+
return `a document with a ${MODE_WORDS[needed]} view — this one declares none`;
|
|
965
|
+
}
|
|
966
|
+
if (preset === "code") {
|
|
967
|
+
/* Refused at CREATION, and refused again for as long as it lasts. Two Code views of one file
|
|
968
|
+
is a second Monaco model on one URI, which real Monaco throws on; this predicate stops the
|
|
969
|
+
lens being MADE that way, and {@link lensTarget} stops it being DRAWN that way when the
|
|
970
|
+
source pane switches to Code afterwards. A comment here once claimed one predicate bought
|
|
971
|
+
the whole of that safety, and one click on the Editor axis was enough to disprove it. */
|
|
972
|
+
return canvasModeOfTab(source) === "source"
|
|
973
|
+
? "a source pane that is not already showing Code"
|
|
974
|
+
: null;
|
|
975
|
+
}
|
|
976
|
+
if (preset === "diff") {
|
|
977
|
+
/* Asked of THIS DOCUMENT's source-control status, not of an app-level slot. It read
|
|
978
|
+
`shell.git.diffState === null`, which is written only when the author clicks a file in the
|
|
979
|
+
Git panel — so the row was refused for a page that genuinely has changes, and offered for a
|
|
980
|
+
page that does not the moment some other file's diff was on screen. */
|
|
981
|
+
return gitChangeFor(source.documentPath) === null ? "a file with changes against HEAD" : null;
|
|
982
|
+
}
|
|
983
|
+
if (preset === "breakpoint") {
|
|
984
|
+
return declaredMedia(source).includes(media)
|
|
985
|
+
? null
|
|
986
|
+
: "a breakpoint this document declares — see Project Settings › Contexts";
|
|
987
|
+
}
|
|
988
|
+
if (preset === "layout") {
|
|
989
|
+
/* REFUSED UP FRONT, because the answer is a stable fact about the document rather than a
|
|
990
|
+
standing rule waiting on the author's next click. Offered anyway, choosing it published a
|
|
991
|
+
derivation whose companion never opened anything: a pane holding a derivation and no tabs,
|
|
992
|
+
which `paneIsEmpty` will not collapse, whose strip drew nothing and whose stage drew nothing.
|
|
993
|
+
`component` is deliberately NOT refused the same way — "select an element inside a component"
|
|
994
|
+
is a rule that resolves on the next click, which is the whole point of leaving it open. */
|
|
995
|
+
return getEffectiveLayoutPath(source.doc.document.$layout as string | false | undefined) ===
|
|
996
|
+
null
|
|
997
|
+
? "a page with a layout — this one declares none"
|
|
998
|
+
: null;
|
|
999
|
+
}
|
|
1000
|
+
if (preset === "locale") {
|
|
1001
|
+
/* THE PROJECT'S OWN LIST, read at the moment the menu is drawn. `projectState` is a plain
|
|
1002
|
+
module-level `let` replaced wholesale by `setProjectState`, so a locale added in Project
|
|
1003
|
+
Settings is visible to the next call and to nothing that cached one. */
|
|
1004
|
+
const i18n = getEffectiveLocales();
|
|
1005
|
+
if (i18n === null || i18n.locales.length < 2) {
|
|
1006
|
+
return "a project that declares more than one locale — see Project Settings › Locales";
|
|
1007
|
+
}
|
|
1008
|
+
/* A PATH, because a translation is a FILE (§13.3) rather than a rendering: an unsaved document
|
|
1009
|
+
has nowhere for its sibling to be, and `translationPathFor` would answer null for it one
|
|
1010
|
+
frame later with a sentence about locales instead of about saving. */
|
|
1011
|
+
if (source.documentPath === null) {
|
|
1012
|
+
return "a document that has been saved";
|
|
1013
|
+
}
|
|
1014
|
+
return locale !== null && i18n.locales.includes(locale)
|
|
1015
|
+
? null
|
|
1016
|
+
: "a locale this project declares";
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Why `paneId` cannot derive, or null when it can. **A pure function OF THE PANE.**
|
|
1023
|
+
*
|
|
1024
|
+
* `pane.derive`'s `enablement` opened with `activePane()` and the menu asked the registry
|
|
1025
|
+
* `disabledReason("pane.derive")` for a pane it had been handed by name — so the same pane got two
|
|
1026
|
+
* answers depending on where the keyboard was, and every row in the SECONDARY pane's menu was
|
|
1027
|
+
* permanently disabled the moment the author clicked into the lens beside it.
|
|
1028
|
+
*
|
|
1029
|
+
* **The structural point, stated once here.** A command's `enablement(ctx)` receives no pane, and
|
|
1030
|
+
* it cannot: a command's subject IS the focused pane, which is what makes `⌘\` mean "split the
|
|
1031
|
+
* thing I am looking at". A per-pane MENU is a different question with a different subject, so it
|
|
1032
|
+
* must not be routed through a predicate that resolves one. The answer therefore lives in a pure
|
|
1033
|
+
* predicate the menu calls with its own pane, and the command record reuses it for the focused one
|
|
1034
|
+
* — one rule, two subjects, no second definition site.
|
|
1035
|
+
*
|
|
1036
|
+
* `scripts/check-pane-singletons.ts` rule 4 charges a pane-scoped function for reading the focus
|
|
1037
|
+
* one hop in, and it could not see this one: the hop was a METHOD CALL on a registry value,
|
|
1038
|
+
* dispatched by a string id to a closure built in another module. Following THAT needs a type
|
|
1039
|
+
* checker. What the rule can decide is that the question was asked, so `disabledReason` and
|
|
1040
|
+
* `isEnabled` are focus reads now — see that file's {@link FOCUS_NAMES}. The closure is still
|
|
1041
|
+
* invisible and is not pretended otherwise.
|
|
1042
|
+
*
|
|
1043
|
+
* @param {string} paneId
|
|
1044
|
+
* @returns {string | null}
|
|
1045
|
+
*/
|
|
1046
|
+
export function deriveRefusal(paneId: string): string | null {
|
|
1047
|
+
const pane = paneById(paneId);
|
|
1048
|
+
return pane && pane.activeTabId !== null && pane.derived === null ? null : DERIVE_REQUIRES;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** The one sentence `pane.derive` refuses with — its `requires`, and the menu's disabled reason. */
|
|
1052
|
+
const DERIVE_REQUIRES = "an open document in a pane that is not itself derived";
|
|
1053
|
+
|
|
1054
|
+
/** The derivation record a preset builds, given the pane it derives from. */
|
|
1055
|
+
function derivationFor(
|
|
1056
|
+
preset: DerivePreset,
|
|
1057
|
+
sourcePaneId: string,
|
|
1058
|
+
media: string | null,
|
|
1059
|
+
locale: string | null = null,
|
|
1060
|
+
): PaneDerivation {
|
|
1061
|
+
/* A COMPANION, because the French copy of this page is a different FILE — §13.3 has no message
|
|
1062
|
+
catalogue, so there is no rendering of this document that is the French one. A lens here would
|
|
1063
|
+
be a second copy of the same page under a chip naming another language, which is the defect
|
|
1064
|
+
this module's own header warns about: a projection that changes only the chip. */
|
|
1065
|
+
if (preset === "locale") {
|
|
1066
|
+
return {
|
|
1067
|
+
kind: "companion",
|
|
1068
|
+
locale,
|
|
1069
|
+
preset,
|
|
1070
|
+
reason: "",
|
|
1071
|
+
resolved: null,
|
|
1072
|
+
sourcePaneId,
|
|
1073
|
+
status: "loading",
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (preset === "layout" || preset === "component") {
|
|
1077
|
+
return {
|
|
1078
|
+
kind: "companion",
|
|
1079
|
+
preset,
|
|
1080
|
+
reason: "",
|
|
1081
|
+
resolved: null,
|
|
1082
|
+
sourcePaneId,
|
|
1083
|
+
status: "loading",
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const mode = preset === "code" ? "source" : preset === "diff" ? "git-diff" : "design";
|
|
1087
|
+
return {
|
|
1088
|
+
/* NO COMPARISON, whatever the Git panel happens to be holding. This seeded itself from
|
|
1089
|
+
`shell.git.diffState` — an app-level slot carrying the file the author last clicked in the
|
|
1090
|
+
Git panel — so a brand-new Diff lens's FIRST PAINT was somebody else's document, under a
|
|
1091
|
+
chip naming this one, until the follow's frame corrected it. The lens loads its own. */
|
|
1092
|
+
diff: null,
|
|
1093
|
+
kind: "lens",
|
|
1094
|
+
media: preset === "breakpoint" ? media : null,
|
|
1095
|
+
mode,
|
|
1096
|
+
preset,
|
|
1097
|
+
reason: "",
|
|
1098
|
+
sourcePaneId,
|
|
1099
|
+
status: "loading",
|
|
1100
|
+
zoom: 1,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Whether `pane` can be pinned — a companion can, a lens cannot.
|
|
1106
|
+
*
|
|
1107
|
+
* A companion names a different FILE, so promoting its preview tab and dropping the derivation
|
|
1108
|
+
* leaves an ordinary tab, exactly as §18.4 says. A lens has no document of its own: pinning it
|
|
1109
|
+
* would have to mint a second tab for a path that already has one, which is the §14.1 violation the
|
|
1110
|
+
* whole design exists to avoid. Its exit is `pane.unsplit`.
|
|
1111
|
+
*/
|
|
1112
|
+
function pinnablePane(): Pane | null {
|
|
1113
|
+
return workspace.panes.find((pane) => pinRefusal(pane.id) === null) ?? null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Why `paneId` cannot be pinned, or null when it can. Pure, like {@link deriveRefusal}.
|
|
1118
|
+
*
|
|
1119
|
+
* **Unlike `pane.derive`, the COMMAND's subject is the grid rather than the focused pane**, and
|
|
1120
|
+
* that is deliberate: the author reaches "Keep This Document" from the palette while the keyboard
|
|
1121
|
+
* is in the page they are editing, and a grid holds at most one derived pane — `pane.derive` puts
|
|
1122
|
+
* the projection in `sidePane()`, and `workspace.ts`'s `MAX_PANES` is 2. {@link pinnablePane} finds
|
|
1123
|
+
* it by scanning, which reads no focus and is therefore the same answer from anywhere — so the
|
|
1124
|
+
* preset menu can ask it about its own pane and get an answer about its own pane.
|
|
1125
|
+
*
|
|
1126
|
+
* @param {string} paneId
|
|
1127
|
+
* @returns {string | null}
|
|
1128
|
+
*/
|
|
1129
|
+
export function pinRefusal(paneId: string): string | null {
|
|
1130
|
+
const pane = paneById(paneId);
|
|
1131
|
+
return pane?.derived?.kind === "companion" && pane.activeTabId ? null : PIN_REQUIRES;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** `pane.pin`'s `requires`, and the menu's disabled reason. One sentence, one home. */
|
|
1135
|
+
const PIN_REQUIRES =
|
|
1136
|
+
"a derived pane showing a document of its own — Code, Diff and breakpoint views project " +
|
|
1137
|
+
"the document already open beside them";
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The derivation commands: `pane.derive` and `pane.pin`.
|
|
1141
|
+
*
|
|
1142
|
+
* They live here rather than in `paneCommands()` because a capability's record belongs beside its
|
|
1143
|
+
* implementation — the same rule the tab and pane commands follow. `pane.compareWith` deliberately
|
|
1144
|
+
* stays in `workspace.ts`: it is a transport verb over the pane model and needs none of this.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {DerivationDeps} deps
|
|
1147
|
+
* @returns {AnyCommand[]}
|
|
1148
|
+
*/
|
|
1149
|
+
export function derivationCommands(deps: DerivationDeps): AnyCommand[] {
|
|
1150
|
+
return [
|
|
1151
|
+
{
|
|
1152
|
+
args: argsSchema(
|
|
1153
|
+
{
|
|
1154
|
+
locale: stringProperty('BCP 47 tag for preset "locale". Ignored by every other preset.'),
|
|
1155
|
+
media: stringProperty(
|
|
1156
|
+
'Breakpoint name for preset "breakpoint" — omit or leave empty for the base size. ' +
|
|
1157
|
+
"Ignored by every other preset.",
|
|
1158
|
+
),
|
|
1159
|
+
preset: enumProperty(
|
|
1160
|
+
DERIVE_PRESETS,
|
|
1161
|
+
"Which projection of this pane's document to show beside it.",
|
|
1162
|
+
),
|
|
1163
|
+
},
|
|
1164
|
+
["preset"],
|
|
1165
|
+
),
|
|
1166
|
+
id: "pane.derive",
|
|
1167
|
+
title: "Show Beside This…",
|
|
1168
|
+
category: "View",
|
|
1169
|
+
level: "document",
|
|
1170
|
+
/* NOT in the palette, and that is a fact about the palette rather than a judgement about
|
|
1171
|
+
this verb: `panels/quick-search.ts`'s `paletteArgs` can prompt for exactly ONE closed value
|
|
1172
|
+
space, so a command with two properties is dropped from command mode entirely. Declaring
|
|
1173
|
+
`"palette"` here would declare a surface that never renders the row — the same
|
|
1174
|
+
"declared and never drawn" defect `context/pane` itself spent two phases being. The preset
|
|
1175
|
+
menu on the pane's context bar is this command's surface, and every row there IS the
|
|
1176
|
+
command with its arguments (§13.5), which is also how the screenshot manifest addresses
|
|
1177
|
+
it. */
|
|
1178
|
+
menus: ["context/pane"],
|
|
1179
|
+
group: "5_pane",
|
|
1180
|
+
when: (ctx) => ctx.document.open,
|
|
1181
|
+
/* One sentence, because `enablement` only has one. What each preset needs is decided at run
|
|
1182
|
+
time by {@link presetRefusal}, which the menu also reads to disable a row with its own
|
|
1183
|
+
reason.
|
|
1184
|
+
The FOCUS is read HERE and only here — a command's subject is the pane the keyboard is in,
|
|
1185
|
+
and that read belongs at the record where a reviewer can see it. {@link deriveRefusal} is
|
|
1186
|
+
the pane-scoped predicate underneath, which is what the per-pane menu calls instead. */
|
|
1187
|
+
enablement: () => deriveRefusal(activePane().id) === null,
|
|
1188
|
+
requires: DERIVE_REQUIRES,
|
|
1189
|
+
undo: "none",
|
|
1190
|
+
aiTool: {
|
|
1191
|
+
description: "Show a projection of the current document in the pane beside it.",
|
|
1192
|
+
name: "show_beside",
|
|
1193
|
+
},
|
|
1194
|
+
run: (_ctx, args) => {
|
|
1195
|
+
const preset = enumArg("pane.derive", args, "preset", DERIVE_PRESETS);
|
|
1196
|
+
/* `??`, not `||`. `optionalStringArg` answers `undefined` or a non-empty string — it is
|
|
1197
|
+
`stringArg` underneath, which REFUSES `""` — so the two operators cannot disagree here,
|
|
1198
|
+
and the `||` implied a blank the caller can never deliver. */
|
|
1199
|
+
const media = optionalStringArg("pane.derive", args, "media") ?? null;
|
|
1200
|
+
const locale = optionalStringArg("pane.derive", args, "locale") ?? null;
|
|
1201
|
+
const source = activePane();
|
|
1202
|
+
const refusal = presetRefusal(preset, source.id, media, locale);
|
|
1203
|
+
if (refusal) {
|
|
1204
|
+
throw new RangeError(
|
|
1205
|
+
`command "pane.derive" argument "preset": "${preset}" requires ${refusal}`,
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
const target = sidePane();
|
|
1209
|
+
/* The DERIVATION IS PUBLISHED FIRST, and the order is not cosmetic.
|
|
1210
|
+
§18.1 rule 3 removes a pane with no subject, and `detachTab` applies it as each tab
|
|
1211
|
+
leaves — so handing the side pane's tabs back before it had a derivation collapsed the
|
|
1212
|
+
very pane about to become the lens, and `setPaneDerivation` then wrote onto a pane that
|
|
1213
|
+
was no longer in the grid. `paneIsEmpty` counts a derivation as a subject; giving the
|
|
1214
|
+
pane one first is what makes the hand-back safe. Nothing renders between these two
|
|
1215
|
+
statements — `run` is synchronous to the end — so no frame observes a lens with tabs. */
|
|
1216
|
+
setPaneDerivation(target.id, derivationFor(preset, source.id, media, locale));
|
|
1217
|
+
/* NOTHING IS CLOSED. Whatever the side pane was holding goes back to the pane the author
|
|
1218
|
+
is in — the same promise `closePane` makes — so a derivation is a layout action rather
|
|
1219
|
+
than a destructive one. */
|
|
1220
|
+
/* `detachTab` reassigns `activeTabId` to `tabOrder.at(-1) ?? null` as each tab leaves, so
|
|
1221
|
+
the pane's own field is already `null` when the last one has gone: a trailing
|
|
1222
|
+
`target.activeTabId = null` here wrote the value it already held, and inverting it
|
|
1223
|
+
changed nothing any state could reach. `applyDerivation` still owns the D2 repair for a
|
|
1224
|
+
tab that arrives afterwards. */
|
|
1225
|
+
const displaced = [...target.tabOrder];
|
|
1226
|
+
for (const tabId of displaced) {
|
|
1227
|
+
detachTab(tabId);
|
|
1228
|
+
insertIntoPane(source, tabId);
|
|
1229
|
+
}
|
|
1230
|
+
applyDerivation(target.id, deps);
|
|
1231
|
+
// The focus does NOT move. An assistant pane does not take the keyboard: the author asked
|
|
1232
|
+
// To SEE something beside what they are doing, not to go and work on it.
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
id: "pane.pin",
|
|
1237
|
+
title: "Keep This Document",
|
|
1238
|
+
category: "View",
|
|
1239
|
+
level: "document",
|
|
1240
|
+
menus: ["context/pane", "palette"],
|
|
1241
|
+
group: "5_pane",
|
|
1242
|
+
/* Enabled for a COMPANION only, and the sentence says which — §18.4's "until it is pinned, at
|
|
1243
|
+
which point it becomes an ordinary tab" is true for `layout` and `component` and impossible
|
|
1244
|
+
for `code`, `diff` and `breakpoint`. */
|
|
1245
|
+
enablement: () => pinnablePane() !== null,
|
|
1246
|
+
requires: PIN_REQUIRES,
|
|
1247
|
+
undo: "none",
|
|
1248
|
+
run: () => {
|
|
1249
|
+
const pane = pinnablePane();
|
|
1250
|
+
if (!pane?.activeTabId) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
promoteTab(pane.activeTabId);
|
|
1254
|
+
clearPaneDerivation(pane.id);
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
];
|
|
1258
|
+
}
|