@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
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { computed, reactive, toRaw } from "../reactivity";
|
|
2
|
+
import { commitTabBuffers } from "../services/monaco-buffer";
|
|
2
3
|
import { ensureCollab, rekeyCollab } from "../collab/collab-session";
|
|
3
4
|
import { createTab, disposeTab } from "../tabs/tab";
|
|
4
|
-
import
|
|
5
|
+
import { projectState } from "../state";
|
|
6
|
+
import {
|
|
7
|
+
argsSchema,
|
|
8
|
+
booleanArg,
|
|
9
|
+
booleanProperty,
|
|
10
|
+
stringArg,
|
|
11
|
+
stringProperty,
|
|
12
|
+
} from "../commands/command-args";
|
|
13
|
+
import type { Tab, TabOrigin } from "../tabs/tab";
|
|
5
14
|
|
|
6
15
|
import type { ComponentEntry } from "../files/components";
|
|
16
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
17
|
+
import type { GitDiffState } from "../types";
|
|
7
18
|
import type { JxMutableNode, JxStyle } from "@jxsuite/schema/types";
|
|
8
19
|
import type { ComputedRef } from "@vue/reactivity";
|
|
9
20
|
|
|
@@ -13,6 +24,135 @@ interface FileEntry {
|
|
|
13
24
|
type: string;
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
/** A tab that was closed, kept so `⌘⇧T` can bring it back. Only file-backed tabs qualify. */
|
|
28
|
+
export interface ClosedTabRecord {
|
|
29
|
+
documentPath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How many closed tabs `⌘⇧T` can walk back through. */
|
|
33
|
+
const CLOSED_TAB_LIMIT = 20;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One editor pane — the unit of split, focus and zoom (§4.1).
|
|
37
|
+
*
|
|
38
|
+
* A tab BELONGS to a pane, and the pane is what splits; the tab strip is per-pane for the same
|
|
39
|
+
* reason. This is the minimal model P3 asks for: an ordered list and an active id, with no rules
|
|
40
|
+
* for derived panes and no follow behaviour (those are P8).
|
|
41
|
+
*/
|
|
42
|
+
export interface Pane {
|
|
43
|
+
id: string;
|
|
44
|
+
/** Left-to-right order in this pane's strip. Pinned tabs occupy the head of it. */
|
|
45
|
+
tabOrder: string[];
|
|
46
|
+
activeTabId: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Non-null when this pane draws a projection of ANOTHER pane rather than a document of its own.
|
|
49
|
+
*
|
|
50
|
+
* The one field §18.4 needed, and the only place the derivation lives. Written by
|
|
51
|
+
* `workspace/pane-derive.ts` and nothing else.
|
|
52
|
+
*/
|
|
53
|
+
derived: PaneDerivation | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What a derived pane is showing, and why it changes when the pane it derives from does.
|
|
58
|
+
*
|
|
59
|
+
* **Two families, because "a projection of another pane's document" turns out to be two mechanisms
|
|
60
|
+
* with different identity consequences**, and conflating them is what made §18.4 look like it
|
|
61
|
+
* violated §14.1.
|
|
62
|
+
*
|
|
63
|
+
* - A **lens** re-draws the SAME document its source pane is showing, in a different mode, at a
|
|
64
|
+
* different breakpoint, or against a different diff. It owns NO tab: `tabOrder` is `[]` and
|
|
65
|
+
* `activeTabId` is `null`, and `tabOfPane` hops through `sourcePaneId` to answer what it draws.
|
|
66
|
+
* No id maps to two documents and no document takes two ids, so §14.1's identity rule is
|
|
67
|
+
* untouched — what changes is only the unstated operational corollary that a tab lived in one
|
|
68
|
+
* pane. Ownership stays one-to-one; DISPLAY becomes many-to-one.
|
|
69
|
+
* - A **companion** shows a DIFFERENT document chosen by a rule from the source — the layout that
|
|
70
|
+
* wraps its page, the component definition under its selection, the same page written in another
|
|
71
|
+
* language. That is an ordinary path-keyed tab which this pane owns normally; the derivation only
|
|
72
|
+
* decides WHICH of its tabs is on screen, which is what `activateTab` has always done.
|
|
73
|
+
*
|
|
74
|
+
* The split is also the answer to what Pin means: a companion can be pinned (it names a different
|
|
75
|
+
* file), a lens cannot (a second tab for the source's own path IS the §14.1 violation) — its exit
|
|
76
|
+
* is `pane.unsplit`.
|
|
77
|
+
*/
|
|
78
|
+
export type PaneDerivation =
|
|
79
|
+
| {
|
|
80
|
+
kind: "lens";
|
|
81
|
+
sourcePaneId: string;
|
|
82
|
+
preset: "code" | "diff" | "breakpoint";
|
|
83
|
+
/** The canvas mode THIS pane draws the shared tab in. Never written onto the tab. */
|
|
84
|
+
mode: string;
|
|
85
|
+
/** `breakpoint` only: the media name, null for base. */
|
|
86
|
+
media: string | null;
|
|
87
|
+
/** `diff` only: this pane's OWN diff, never `shell.git.diffState`. */
|
|
88
|
+
diff: GitDiffState | null;
|
|
89
|
+
/**
|
|
90
|
+
* This pane's own scale. `session.ui.zoom` is per-TAB, so under a lens the mobile view and
|
|
91
|
+
* the desktop view it is a lens OF would zoom together.
|
|
92
|
+
*/
|
|
93
|
+
zoom: number;
|
|
94
|
+
status: DerivationStatus;
|
|
95
|
+
/** Printed by the pane when `status` is `unavailable`. Empty otherwise. */
|
|
96
|
+
reason: string;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
kind: "companion";
|
|
100
|
+
sourcePaneId: string;
|
|
101
|
+
preset: "layout" | "component";
|
|
102
|
+
/** The follow's memo: the path this derivation last resolved to. */
|
|
103
|
+
resolved: string | null;
|
|
104
|
+
status: DerivationStatus;
|
|
105
|
+
reason: string;
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
* The `locale` companion is a THIRD member rather than a `locale: string | null` on the arm
|
|
109
|
+
* above, because a locale is the one thing this preset cannot be without: `companionTarget` has
|
|
110
|
+
* no answer at all for a locale derivation that names no locale, and a field every other
|
|
111
|
+
* companion carries as `null` forever is a field the type system stops guarding. Narrowing on
|
|
112
|
+
* `preset === "locale"` — which the resolver and the strip chip both already do to say anything
|
|
113
|
+
* useful — is what makes the tag reachable, so nothing reads it without having asked which
|
|
114
|
+
* preset this is.
|
|
115
|
+
*/
|
|
116
|
+
| {
|
|
117
|
+
kind: "companion";
|
|
118
|
+
sourcePaneId: string;
|
|
119
|
+
preset: "locale";
|
|
120
|
+
/**
|
|
121
|
+
* The canonical BCP 47 tag this pane holds. Null only while `pane.derive` was given none —
|
|
122
|
+
* {@link presetRefusal} refuses that, and the pane says so rather than opening a file.
|
|
123
|
+
*/
|
|
124
|
+
locale: string | null;
|
|
125
|
+
/** The follow's memo: the path this derivation last resolved to. */
|
|
126
|
+
resolved: string | null;
|
|
127
|
+
status: DerivationStatus;
|
|
128
|
+
reason: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Where a derivation is in its own lifecycle. `unavailable` DRAWS; it never collapses the pane. */
|
|
132
|
+
export type DerivationStatus = "loading" | "ready" | "unavailable";
|
|
133
|
+
|
|
134
|
+
export const PRIMARY_PANE = "primary";
|
|
135
|
+
|
|
136
|
+
export const SECONDARY_PANE = "secondary";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Two, and enforced.
|
|
140
|
+
*
|
|
141
|
+
* Not a UI preference and not a placeholder: every additional Canvas pane is a real
|
|
142
|
+
* `@jxsuite/runtime` render, an `iframe-channel` connection and a structured clone of the resolved
|
|
143
|
+
* document, all on one main thread. Two is a budget that was measured; three is a cliff.
|
|
144
|
+
*
|
|
145
|
+
* This is the ONE cap left. `SECONDARY_PANE_KINDS` used to sit beside it — Code, Diff, Config,
|
|
146
|
+
* Entry, Grid, Library, the cheap kinds — because a second LIVE host was unaffordable while the
|
|
147
|
+
* shell had one stage to hand between panes and one app-wide render generation to invalidate.
|
|
148
|
+
* Neither is true: `canvas/surface-registry.ts` gives every pane its own panels, mode, pan, Monaco
|
|
149
|
+
* and generation, and `panels/pane-grid.ts` gives every pane its own stage. A Canvas in the side
|
|
150
|
+
* pane is now the same object the primary has always had, so the kind cap has nothing left to
|
|
151
|
+
* protect and every predicate that read it is deleted with it — including the one that FLIPPED a
|
|
152
|
+
* splitting Design tab to Code on its way across.
|
|
153
|
+
*/
|
|
154
|
+
export const MAX_PANES = 2;
|
|
155
|
+
|
|
16
156
|
export interface Workspace {
|
|
17
157
|
projectRoot: string | null;
|
|
18
158
|
projectConfig: object | null;
|
|
@@ -26,37 +166,494 @@ export interface Workspace {
|
|
|
26
166
|
searchQuery: string;
|
|
27
167
|
};
|
|
28
168
|
tabs: Map<string, Tab>;
|
|
29
|
-
|
|
30
|
-
|
|
169
|
+
/** One or two panes, `panes[0]` always the primary. The pane grid, as data. */
|
|
170
|
+
panes: Pane[];
|
|
171
|
+
/** Which pane the keyboard and every "the active document" read resolve through. */
|
|
172
|
+
activePaneId: string;
|
|
173
|
+
/**
|
|
174
|
+
* Most-recently-used order, newest first, ACROSS panes. `⌃Tab` walks this, NOT
|
|
175
|
+
* {@link Pane.tabOrder} — "the tab I was just in" is the one people reach for, and it is rarely
|
|
176
|
+
* the one to the left. Cycling onto a tab in the other pane focuses that pane too.
|
|
177
|
+
*/
|
|
178
|
+
mruOrder: string[];
|
|
179
|
+
/** Newest first. `⌘⇧T` pops this. */
|
|
180
|
+
closedTabs: ClosedTabRecord[];
|
|
31
181
|
ui: { activityBar: string };
|
|
182
|
+
/**
|
|
183
|
+
* The focused pane's strip order.
|
|
184
|
+
*
|
|
185
|
+
* A derived read, not a second store: with panes, "the workspace's tab order" can only mean the
|
|
186
|
+
* order of the pane you are in. Assigning to it is a type error on purpose — every write goes
|
|
187
|
+
* through a pane.
|
|
188
|
+
*/
|
|
189
|
+
readonly tabOrder: readonly string[];
|
|
190
|
+
/** The focused pane's active tab. Derived from {@link Workspace.panes} for the same reason. */
|
|
191
|
+
readonly activeTabId: string | null;
|
|
32
192
|
}
|
|
33
193
|
|
|
34
194
|
// Annotated as Workspace: Vue's UnwrapNestedRefs cannot terminate on the
|
|
35
195
|
// Recursive document types, and reactive() does not unwrap refs we never use.
|
|
36
196
|
export const workspace: Workspace = reactive({
|
|
37
|
-
|
|
197
|
+
activePaneId: PRIMARY_PANE,
|
|
38
198
|
clipboard: null as JxMutableNode | null,
|
|
199
|
+
closedTabs: [] as ClosedTabRecord[],
|
|
39
200
|
componentRegistry: [] as ComponentEntry[],
|
|
201
|
+
mruOrder: [] as string[],
|
|
40
202
|
fileTree: {
|
|
41
203
|
dirs: new Map<string, FileEntry[]>(),
|
|
42
204
|
expanded: new Set<string>(),
|
|
43
205
|
searchQuery: "",
|
|
44
206
|
selectedPath: null as string | null,
|
|
45
207
|
},
|
|
208
|
+
panes: [
|
|
209
|
+
{
|
|
210
|
+
activeTabId: null as string | null,
|
|
211
|
+
derived: null as PaneDerivation | null,
|
|
212
|
+
id: PRIMARY_PANE,
|
|
213
|
+
tabOrder: [] as string[],
|
|
214
|
+
},
|
|
215
|
+
],
|
|
46
216
|
projectConfig: null as object | null,
|
|
47
217
|
projectRoot: null as string | null,
|
|
48
218
|
styleClipboard: null as JxStyle | null,
|
|
49
|
-
tabOrder: [] as string[],
|
|
50
219
|
tabs: new Map<string, Tab>(),
|
|
51
220
|
ui: {
|
|
52
221
|
activityBar: "files",
|
|
53
222
|
},
|
|
223
|
+
// The two derived reads. Defined as getters on the object reactive() wraps, so a consumer that
|
|
224
|
+
// Reads `workspace.activeTabId` inside an effect tracks `panes` and `activePaneId` — the same
|
|
225
|
+
// Dependency it used to have on the field these replace, with no second source of truth.
|
|
226
|
+
get activeTabId(): string | null {
|
|
227
|
+
/* THE LENS HOP, and it is here rather than at any reader because this getter is the only
|
|
228
|
+
thing that can observe an un-hopped `activePane().activeTabId` (its two other readers are
|
|
229
|
+
both inside this object literal).
|
|
230
|
+
|
|
231
|
+
Without it, focusing a lens pane makes `activeTab.value` null and the Inspector, the
|
|
232
|
+
toolbar, ⌘S, undo and every `ctx.document.open` print "no document" over a stage that is
|
|
233
|
+
visibly drawing one — §18.1 rule 1's exact recorded failure, mintable by one click. A lens
|
|
234
|
+
has no document of its own, so "the document I am in" is the source pane's; ⌘W in a focused
|
|
235
|
+
lens pane therefore closes that document, which is coherent and collapses the lens. */
|
|
236
|
+
const pane = activePane();
|
|
237
|
+
return pane.derived?.kind === "lens"
|
|
238
|
+
? (paneById(pane.derived.sourcePaneId)?.activeTabId ?? null)
|
|
239
|
+
: pane.activeTabId;
|
|
240
|
+
},
|
|
241
|
+
get tabOrder(): readonly string[] {
|
|
242
|
+
return activePane().tabOrder;
|
|
243
|
+
},
|
|
54
244
|
}) as unknown as Workspace;
|
|
55
245
|
|
|
56
246
|
export const activeTab = computed(() =>
|
|
57
247
|
workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
|
|
58
248
|
) as unknown as ComputedRef<Tab | null>;
|
|
59
249
|
|
|
250
|
+
// ─── Panes ────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The focused pane. Never null: `panes[0]` is created with the store and is never removed, so
|
|
254
|
+
* "where am I" always has an answer even with no project open.
|
|
255
|
+
*
|
|
256
|
+
* @returns {Pane}
|
|
257
|
+
*/
|
|
258
|
+
export function activePane(): Pane {
|
|
259
|
+
const { panes } = workspace;
|
|
260
|
+
return panes.find((pane) => pane.id === workspace.activePaneId) ?? panes[0]!;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {string} paneId
|
|
265
|
+
* @returns {Pane | undefined}
|
|
266
|
+
*/
|
|
267
|
+
export function paneById(paneId: string): Pane | undefined {
|
|
268
|
+
return workspace.panes.find((pane) => pane.id === paneId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The pane holding `tabId`, or undefined when no pane does.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} tabId
|
|
275
|
+
* @returns {Pane | undefined}
|
|
276
|
+
*/
|
|
277
|
+
export function paneOfTab(tabId: string): Pane | undefined {
|
|
278
|
+
return workspace.panes.find((pane) => pane.tabOrder.includes(tabId));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Move focus to a pane. A no-op for an id no pane carries, so a stale persisted layout cannot
|
|
283
|
+
* strand the keyboard in a pane that is not there.
|
|
284
|
+
*
|
|
285
|
+
* **And a no-op for the pane that already has it**, which is what makes it safe to call on every
|
|
286
|
+
* pointerdown. Moving focus is not only the `activePaneId` write — Vue skips that one when the
|
|
287
|
+
* value is unchanged — it is also `resetTabCycle` (which abandons a live ⌘-Tab walk), `promoteMru`
|
|
288
|
+
* (which rewrites the MRU order every ⌘-Tab reads) and `syncTreeSelection` (which moves the
|
|
289
|
+
* Library's cursor). Running those on a click that changed nothing would make every click in the
|
|
290
|
+
* focused pane a small act of state destruction, so the guard is HERE rather than at the call site:
|
|
291
|
+
* this is the module that owns focus, and a caller holding a `paneId` may not ask which pane has it
|
|
292
|
+
* (`scripts/check-pane-singletons.ts` rule 4).
|
|
293
|
+
*
|
|
294
|
+
* @param {string} paneId
|
|
295
|
+
*/
|
|
296
|
+
export function focusPane(paneId: string) {
|
|
297
|
+
const pane = paneById(paneId);
|
|
298
|
+
if (!pane || pane.id === workspace.activePaneId) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
workspace.activePaneId = pane.id;
|
|
302
|
+
resetTabCycle();
|
|
303
|
+
const { activeTabId } = pane;
|
|
304
|
+
if (activeTabId) {
|
|
305
|
+
promoteMru(activeTabId);
|
|
306
|
+
const tab = workspace.tabs.get(activeTabId);
|
|
307
|
+
if (tab) {
|
|
308
|
+
syncTreeSelection(tab);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Whether `paneId` is the pane the keyboard is in.
|
|
315
|
+
*
|
|
316
|
+
* A one-line predicate that exists so a pane-scoped renderer can DRAW the focus without READING it.
|
|
317
|
+
* `panels/tab-strip.ts` is the case: its strip carries a `focused` class, so "which pane has focus"
|
|
318
|
+
* is genuinely part of what it paints — but it is handed a pane, and
|
|
319
|
+
* `scripts/check-pane-singletons.ts` rule 4 (rightly) refuses `pane.id === workspace.activePaneId`
|
|
320
|
+
* inside a function that was told which pane it is about. Asking the module that OWNS focus is the
|
|
321
|
+
* distinction the rule is drawing: a surface may render the answer, it may not resolve its subject
|
|
322
|
+
* from it.
|
|
323
|
+
*
|
|
324
|
+
* @param {string} paneId
|
|
325
|
+
* @returns {boolean}
|
|
326
|
+
*/
|
|
327
|
+
export function isPaneFocused(paneId: string): boolean {
|
|
328
|
+
return workspace.activePaneId === paneId;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Focus the pane that is not the focused one, when there is one. */
|
|
332
|
+
export function focusOtherPane() {
|
|
333
|
+
const other = workspace.panes.find((pane) => pane.id !== workspace.activePaneId);
|
|
334
|
+
if (other) {
|
|
335
|
+
focusPane(other.id);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/*
|
|
340
|
+
* There is no `paneCanHostKind`, no `canOpenInSecondPane`, no `hostableKindsOf`, no
|
|
341
|
+
* `paneOfTabCanHostMode` and no `capToPaneKind`.
|
|
342
|
+
*
|
|
343
|
+
* Five predicates, one cap: "a pane other than the primary may only host Code, Diff, Config, Entry,
|
|
344
|
+
* Grid or Library". Two of them enforced it (the split, and every later write of
|
|
345
|
+
* `session.ui.canvasMode`), one picked the replacement kind, and two narrowed what the controls
|
|
346
|
+
* offered so no dropdown could contain an entry the app would refuse.
|
|
347
|
+
*
|
|
348
|
+
* The cap existed because a second LIVE canvas host was unaffordable, and it is gone because
|
|
349
|
+
* workstream 1 made it affordable — see {@link MAX_PANES}. Its LAST act was a bug: `capToPaneKind`
|
|
350
|
+
* flipped a splitting Design tab to `source` before focus moved, so `⌘\` on a page you were
|
|
351
|
+
* designing silently became "open this as Code somewhere else".
|
|
352
|
+
*
|
|
353
|
+
* What replaces them is `tabs/tab.ts`'s `editorKindsOf` — the kinds a DOCUMENT declares — which is
|
|
354
|
+
* the only narrowing left and the only one that was ever about the document rather than about what
|
|
355
|
+
* the shell could afford to draw.
|
|
356
|
+
*/
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Split the grid and move the focused tab into the new pane, which becomes focused.
|
|
360
|
+
*
|
|
361
|
+
* Moves rather than duplicates: one tab is one document with one undo history and one collab
|
|
362
|
+
* session, and two strips claiming the same id is the duplicate-`repeat`-key bug §4.3 describes.
|
|
363
|
+
*
|
|
364
|
+
* @returns {Pane | null} The pane the tab landed in, or null when the split was refused
|
|
365
|
+
*/
|
|
366
|
+
export function splitRight(): Pane | null {
|
|
367
|
+
const source = activePane();
|
|
368
|
+
const tabId = source.activeTabId;
|
|
369
|
+
const tab = tabId ? workspace.tabs.get(tabId) : null;
|
|
370
|
+
if (!tabId || !tab) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
/* The tab moves AS IT IS. `capToPaneKind` used to run here and rewrite `session.ui.canvasMode`
|
|
374
|
+
when the target pane could not host the tab's kind — which, for a Design tab, meant `⌘\`
|
|
375
|
+
silently reopened your page as Code in the pane you had just made. Both panes host every kind
|
|
376
|
+
now, so a split is a move and nothing else. */
|
|
377
|
+
const target = receivingPane();
|
|
378
|
+
detachTab(tabId);
|
|
379
|
+
insertIntoPane(target, tabId);
|
|
380
|
+
/* Focus moves LAST, and every write above went through {@link addPane}'s reactive record.
|
|
381
|
+
Publishing the focus first made the pane observable while it still showed nothing: the
|
|
382
|
+
`activeTab` computed re-ran, cached null, and the jump bar, the Inspector and the toolbar all
|
|
383
|
+
printed "no document" over a stage that was drawing one. The write that should have corrected
|
|
384
|
+
them notified nobody, because it went through the raw literal this used to push. */
|
|
385
|
+
target.activeTabId = tabId;
|
|
386
|
+
workspace.activePaneId = target.id;
|
|
387
|
+
resetTabCycle();
|
|
388
|
+
promoteMru(tabId);
|
|
389
|
+
return target;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The pane BESIDE the focused one, created if the grid has only one.
|
|
394
|
+
*
|
|
395
|
+
* The `existing ?? addPane(SECONDARY_PANE)` shape {@link splitRight} has always used, named because
|
|
396
|
+
* three callers now want it: the split, `pane.compareWith` and `pane.derive`. Unlike `splitRight`
|
|
397
|
+
* it moves NOTHING — "give me the other pane" and "send my document to it" are two requests, and
|
|
398
|
+
* only the split makes both.
|
|
399
|
+
*
|
|
400
|
+
* `SECONDARY_PANE` unconditionally, and it is free unconditionally: {@link closePane} refuses to
|
|
401
|
+
* remove the primary, so "there is no other pane" can only mean the grid is exactly `[primary]` and
|
|
402
|
+
* the focused pane is that primary. This used to be `existing?.id ?? SECONDARY_PANE` handed
|
|
403
|
+
* straight to `addPane` with no check that the id was free — which, on a grid of `["secondary"]`,
|
|
404
|
+
* pushed a SECOND record under that id and gave lit's keyed `repeat` a duplicate key. Both ends are
|
|
405
|
+
* closed: the state is unreachable, and {@link addPane} would not mint the duplicate even if it
|
|
406
|
+
* were. {@link MAX_PANES} is therefore satisfied by construction — there are two ids in play.
|
|
407
|
+
*
|
|
408
|
+
* @returns {Pane}
|
|
409
|
+
*/
|
|
410
|
+
export function sidePane(): Pane {
|
|
411
|
+
const source = activePane();
|
|
412
|
+
return workspace.panes.find((pane) => pane.id !== source.id) ?? addPane(SECONDARY_PANE);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The pane beside the focused one, ready to OWN a tab.
|
|
417
|
+
*
|
|
418
|
+
* {@link sidePane} answers "which pane is beside me". This answers "which pane may I put a document
|
|
419
|
+
* IN", and they differ by exactly one case — a LENS, whose invariant D2 is that it owns no tab at
|
|
420
|
+
* all. Three callers mean the second question and all three were asking the first:
|
|
421
|
+
*
|
|
422
|
+
* - `splitRight` moved the focused tab into a lens. Between that write and the follow's next frame
|
|
423
|
+
* the pane held both a derivation and a tab — D2 violated and observable — and `applyDerivation`
|
|
424
|
+
* then handed the tab straight back, so `⌘\` left the author looking at a different document,
|
|
425
|
+
* their tab at the back of the primary's strip and the keyboard in a pane that owned nothing.
|
|
426
|
+
* - `pane.compareWith` landed its tab in the lens's `tabOrder`, where `tabOfPane` hopped straight
|
|
427
|
+
* past it to the source pane: the compared document was in no strip and on no stage, and the
|
|
428
|
+
* follow — which tracks `derived` and the SOURCE pane's tab, neither of which a tab insertion
|
|
429
|
+
* touches — never ran to repair it.
|
|
430
|
+
* - `navigateToComponent` read `tabOfPane(target.id)` back and got the SOURCE tab, so the §14.2
|
|
431
|
+
* `openedFrom` relationship the function exists to record was silently skipped.
|
|
432
|
+
*
|
|
433
|
+
* **The derivation is released rather than the request refused.** "Put this document over there" is
|
|
434
|
+
* an explicit instruction about the pane a projection is currently borrowing, and a projection is a
|
|
435
|
+
* view with no state of its own to lose — releasing it costs the author nothing and refusing costs
|
|
436
|
+
* them the gesture.
|
|
437
|
+
*
|
|
438
|
+
* **A COMPANION is released too, and leaving it alone was the defect.** "It owns real tabs, so the
|
|
439
|
+
* new one simply joins them" is true of the TABS and false of everything else. A layout companion
|
|
440
|
+
* that has just been handed the page the author split in is still following the source pane's
|
|
441
|
+
* `$layout`, so the next click on layout chrome re-points it and throws the new document off
|
|
442
|
+
* screen; its `tabOrder` is no longer empty, so `panels/tab-strip.ts` draws tab chips instead of
|
|
443
|
+
* the derivation chip and the pane loses both its name and its ✕; and `pane.pin` — which promotes
|
|
444
|
+
* `activeTabId` — promotes whichever document is on top, which after `⌘\` is the page, not the
|
|
445
|
+
* layout the author was keeping. A gesture that empties a derivation of its meaning ends it.
|
|
446
|
+
*
|
|
447
|
+
* @returns {Pane}
|
|
448
|
+
*/
|
|
449
|
+
export function receivingPane(): Pane {
|
|
450
|
+
const target = sidePane();
|
|
451
|
+
if (target.derived) {
|
|
452
|
+
/* The same write {@link closePane} makes for D4, for the same reason and in the same module:
|
|
453
|
+
clearing a derivation is a fact about the PANE GRID. `pane-derive.ts`'s
|
|
454
|
+
`clearPaneDerivation` is the reader-facing spelling and it imports from here, so calling it
|
|
455
|
+
would be a cycle for one field assignment. */
|
|
456
|
+
target.derived = null;
|
|
457
|
+
}
|
|
458
|
+
return target;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Publish a new pane and hand back the REACTIVE record.
|
|
463
|
+
*
|
|
464
|
+
* The pane is constructed HERE so no caller can hold the raw literal. `reactive()` wraps on READ:
|
|
465
|
+
* an object pushed into the array is still the plain object the caller has a reference to, and a
|
|
466
|
+
* write through that reference reaches the same fields while notifying no effect at all. Building
|
|
467
|
+
* the record inside the one function that publishes it is what makes that mistake unavailable
|
|
468
|
+
* rather than merely absent — see the same failure recorded for nested reactive collections.
|
|
469
|
+
*
|
|
470
|
+
* @param {string} paneId
|
|
471
|
+
* @returns {Pane}
|
|
472
|
+
*/
|
|
473
|
+
function addPane(paneId: string): Pane {
|
|
474
|
+
/* A pane id is unique BY CONSTRUCTION, not by every caller checking first. `workspace.panes` is
|
|
475
|
+
the array lit's keyed `repeat` walks, and two records under one key is undefined behaviour
|
|
476
|
+
there — the grid draws two cells for one pane, each `ref` overwriting the other's surface
|
|
477
|
+
record. Handing back the record that is already published is the only answer that cannot
|
|
478
|
+
produce one. */
|
|
479
|
+
const existing = paneById(paneId);
|
|
480
|
+
if (existing) {
|
|
481
|
+
return existing;
|
|
482
|
+
}
|
|
483
|
+
workspace.panes = [
|
|
484
|
+
...workspace.panes,
|
|
485
|
+
{
|
|
486
|
+
activeTabId: null as string | null,
|
|
487
|
+
derived: null as PaneDerivation | null,
|
|
488
|
+
id: paneId,
|
|
489
|
+
tabOrder: [] as string[],
|
|
490
|
+
},
|
|
491
|
+
];
|
|
492
|
+
return paneById(paneId)!;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Collapse a pane, moving its tabs back into the one that remains.
|
|
497
|
+
*
|
|
498
|
+
* Closing a pane must never close documents — that is the difference between a layout action and a
|
|
499
|
+
* destructive one, and only the second is allowed to lose work.
|
|
500
|
+
*
|
|
501
|
+
* **The PRIMARY never leaves the grid, and asking for it collapses the other pane instead.** §18.1
|
|
502
|
+
* rule 3 — a pane with nothing in it is a hole in the grid — held on one side only until
|
|
503
|
+
* `collapseEmptiedPane` was written, and that helper stated the rule a second time: `PRIMARY_PANE`
|
|
504
|
+
* is the id nine screenshots crop, the one `resolveRegion("pane")` canonicalises onto, and
|
|
505
|
+
* `panes[0]`, so it must never be the pane that goes. Stating it in a helper left this function
|
|
506
|
+
* exported and unguarded — `closePane(PRIMARY_PANE)` produced `panes = ["secondary"]`, a
|
|
507
|
+
* `resolveRegion("pane")` of null, and a `splitRight` that then minted a duplicate `repeat` key.
|
|
508
|
+
* Both callers happened to guard; the function did not. It does now, and the helper is gone,
|
|
509
|
+
* because the rule wanted one home rather than two.
|
|
510
|
+
*
|
|
511
|
+
* @param {string} paneId The pane to collapse. `PRIMARY_PANE` collapses the other one.
|
|
512
|
+
*/
|
|
513
|
+
export function closePane(paneId: string) {
|
|
514
|
+
if (workspace.panes.length < 2) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const closingId =
|
|
518
|
+
paneId === PRIMARY_PANE
|
|
519
|
+
? workspace.panes.find((candidate) => candidate.id !== PRIMARY_PANE)?.id
|
|
520
|
+
: paneId;
|
|
521
|
+
const pane = closingId === undefined ? undefined : paneById(closingId);
|
|
522
|
+
const survivor = workspace.panes.find((candidate) => candidate.id !== closingId);
|
|
523
|
+
if (!pane || !survivor) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
/* The ORDER is the whole of this function.
|
|
527
|
+
"The active pane does not exist" must never be observable. Every reader resolves through
|
|
528
|
+
`workspace.activePaneId` — the stage, `tabOfPane`, the jump bar, the Inspector — so removing
|
|
529
|
+
the pane while the focus still names it publishes, for one synchronous instant, a workspace
|
|
530
|
+
whose focused pane has no tab. A render scheduled in that instant tore the canvas down to its
|
|
531
|
+
welcome state and NOTHING scheduled another: the focus flip that followed changed no
|
|
532
|
+
`activeTab`, so both canvas effects stayed quiet and the editor was gone until a reload.
|
|
533
|
+
So: the survivor takes the tabs and the focus while both panes are still in the grid, and only
|
|
534
|
+
then does the closing pane leave it. */
|
|
535
|
+
for (const tabId of pane.tabOrder) {
|
|
536
|
+
insertIntoPane(survivor, tabId);
|
|
537
|
+
}
|
|
538
|
+
/* The document you were LOOKING AT follows you; the one you were not does not replace it.
|
|
539
|
+
This was `pane.activeTabId ?? survivor.activeTabId` unconditionally, so Unsplit run from the
|
|
540
|
+
primary — which closes the SIDE pane, never the focused one — swapped the primary's document
|
|
541
|
+
for the side pane's: focused=primary showing=a became focused=primary showing=b, and the only
|
|
542
|
+
way back was the tab strip. Nothing is lost either way, because the closing pane's tabs have
|
|
543
|
+
just been moved into the survivor above; this only decides which of them is on screen. */
|
|
544
|
+
survivor.activeTabId =
|
|
545
|
+
workspace.activePaneId === pane.id
|
|
546
|
+
? (pane.activeTabId ?? survivor.activeTabId)
|
|
547
|
+
: (survivor.activeTabId ?? pane.activeTabId);
|
|
548
|
+
workspace.activePaneId = survivor.id;
|
|
549
|
+
/* Invariant D4, and it goes HERE — after the survivor has inherited the tabs and the focus, and
|
|
550
|
+
before the closing pane leaves the grid, inside the ordering this function's comment insists
|
|
551
|
+
on. A derived pane's source is mintable in either direction (derive from the secondary and the
|
|
552
|
+
PRIMARY becomes the lens), so a survivor deriving from the pane that is leaving would be a
|
|
553
|
+
pane whose `sourcePaneId` names nothing: `tabOfPane` would answer null and the grid would hold
|
|
554
|
+
a stage drawing a document that no longer has a pane. Clearing it leaves you looking at the
|
|
555
|
+
document, which is what unsplitting a Code lens should give you.
|
|
556
|
+
|
|
557
|
+
There is no RESTORE path beside it, deliberately: a branch nothing can enter is the
|
|
558
|
+
`documentStack` failure §14.3 records at length. */
|
|
559
|
+
if (survivor.derived?.sourcePaneId === pane.id) {
|
|
560
|
+
survivor.derived = null;
|
|
561
|
+
}
|
|
562
|
+
workspace.panes = workspace.panes.filter((candidate) => candidate.id !== pane.id);
|
|
563
|
+
resetTabCycle();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* §18.1 rule 3, as a predicate: **a pane with no SUBJECT is a hole in the grid.**
|
|
568
|
+
*
|
|
569
|
+
* The rule used to be spelled `tabOrder.length === 0` at both sites that apply it, which was the
|
|
570
|
+
* same question while every pane's subject was its tabs. A lens pane's subject is its derivation —
|
|
571
|
+
* its `tabOrder` is empty BY DESIGN — so the literal test would collapse it the instant it was
|
|
572
|
+
* created.
|
|
573
|
+
*/
|
|
574
|
+
function paneIsEmpty(pane: Pane): boolean {
|
|
575
|
+
return pane.tabOrder.length === 0 && pane.derived === null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* No `togglePaneZoom`. Zoom is a view of a GRID, and there is no grid: §18.3 hands the one stage
|
|
579
|
+
between panes, so the focused pane already fills the shell and the other one is not on screen to
|
|
580
|
+
be zoomed away from. The state existed, both commands wrote it, and nothing that draws ever read
|
|
581
|
+
it — a control whose only observable effect is on itself. It comes back with the second live
|
|
582
|
+
host, and not one release earlier. */
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Remove a tab id from whichever pane holds it, without touching the tab itself.
|
|
586
|
+
*
|
|
587
|
+
* Exported for `workspace/pane-derive.ts`, which needs both halves of a move: `pane.derive` hands
|
|
588
|
+
* the side pane's tabs BACK to the pane the author is in before publishing a lens there, because a
|
|
589
|
+
* derivation is a layout action and nothing may be closed by one.
|
|
590
|
+
*/
|
|
591
|
+
export function detachTab(tabId: string) {
|
|
592
|
+
const emptied: string[] = [];
|
|
593
|
+
for (const pane of workspace.panes) {
|
|
594
|
+
if (!pane.tabOrder.includes(tabId)) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
pane.tabOrder = pane.tabOrder.filter((id) => id !== tabId);
|
|
598
|
+
if (pane.activeTabId === tabId) {
|
|
599
|
+
pane.activeTabId = pane.tabOrder.at(-1) ?? null;
|
|
600
|
+
}
|
|
601
|
+
/* The PRIMARY is exempt HERE and only here. `splitRight` moves the tab out of the source pane
|
|
602
|
+
through this function, so a lone document split from the primary empties it — and that state
|
|
603
|
+
is the one the author just asked for, a welcome screen beside the document they sent across.
|
|
604
|
+
Closing a document is a different request, and `closeTab` collapses on both sides. */
|
|
605
|
+
if (pane.id !== PRIMARY_PANE && paneIsEmpty(pane)) {
|
|
606
|
+
emptied.push(pane.id);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// The same rule `closeTab` applies, applied wherever a pane empties: a pane with nothing in it is
|
|
610
|
+
// A hole in the grid. `splitRight` from the side pane moves the tab back to the primary and used
|
|
611
|
+
// To leave the empty pane standing with `pane.focusSecondary` still enabled on it — three
|
|
612
|
+
// Keystrokes to a shell with no stage, no strip, no jump bar and two documents still open. It was
|
|
613
|
+
// Not `closePane`'s ordering bug, which is fixed: "the active pane exists and has no tab"
|
|
614
|
+
// Produces the identical empty shell, and only this stops it being mintable.
|
|
615
|
+
for (const paneId of emptied) {
|
|
616
|
+
closePane(paneId);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* There is no `collapseEmptiedPane`. It existed to say "collapse whichever side emptied, but never
|
|
621
|
+
remove the primary — collapse the other one instead", which is a rule about what CLOSING a pane
|
|
622
|
+
means rather than about emptying one, and {@link closePane} now states it where the removal
|
|
623
|
+
happens. Two homes for one rule is how `closePane` stayed exported and unguarded while both of
|
|
624
|
+
its callers were careful. */
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Put a tab id into a pane at the right place: after the pinned prefix for an ordinary tab, at the
|
|
628
|
+
* end of it for a pinned one. Idempotent.
|
|
629
|
+
*
|
|
630
|
+
* Exported for `workspace/pane-derive.ts` — see {@link detachTab}.
|
|
631
|
+
*/
|
|
632
|
+
export function insertIntoPane(pane: Pane, tabId: string, index?: number) {
|
|
633
|
+
if (pane.tabOrder.includes(tabId)) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const at = index ?? defaultSlot(pane, tabId);
|
|
637
|
+
pane.tabOrder = [...pane.tabOrder.slice(0, at), tabId, ...pane.tabOrder.slice(at)];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** How many tabs at the head of `pane` are pinned. */
|
|
641
|
+
function pinnedCount(pane: Pane): number {
|
|
642
|
+
let count = 0;
|
|
643
|
+
for (const id of pane.tabOrder) {
|
|
644
|
+
if (workspace.tabs.get(id)?.pinned !== true) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
count += 1;
|
|
648
|
+
}
|
|
649
|
+
return count;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** The slot a newly-arriving tab takes: inside the pinned prefix if pinned, else at the end. */
|
|
653
|
+
function defaultSlot(pane: Pane, tabId: string): number {
|
|
654
|
+
return workspace.tabs.get(tabId)?.pinned === true ? pinnedCount(pane) : pane.tabOrder.length;
|
|
655
|
+
}
|
|
656
|
+
|
|
60
657
|
/**
|
|
61
658
|
* Record the open project on the reactive workspace. `root` must be the absolute project root (the
|
|
62
659
|
* same value `createProject` returns and the pending-agent-prompt handoff is keyed by) — not the
|
|
@@ -84,9 +681,127 @@ export function isTabActive(tab: Tab | null): boolean {
|
|
|
84
681
|
);
|
|
85
682
|
}
|
|
86
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Whether `tab` is still open.
|
|
686
|
+
*
|
|
687
|
+
* The question a CAPTURED tab owes before anything is written into it. Both Monaco surfaces commit
|
|
688
|
+
* on a debounce, and a debounce is a promise to write into the document a tab held half a second
|
|
689
|
+
* ago — by which time the tab may have been closed, so `workspace.tabs` no longer holds it and
|
|
690
|
+
* nothing else will ever look at what was written. Deliberately not `isTabActive`: a commit belongs
|
|
691
|
+
* to the tab its editor was mounted for whether or not that tab is the one on screen when the timer
|
|
692
|
+
* fires, and resolving it through the FOCUSED tab is precisely the cross-document write this
|
|
693
|
+
* replaces.
|
|
694
|
+
*
|
|
695
|
+
* @param {Tab | null | undefined} tab
|
|
696
|
+
* @returns {boolean}
|
|
697
|
+
*/
|
|
698
|
+
export function tabIsLive(tab: Tab | null | undefined): boolean {
|
|
699
|
+
if (!tab) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
const held = workspace.tabs.get(tab.id);
|
|
703
|
+
return held !== undefined && toRaw(held as object) === toRaw(tab as unknown as object);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Move `tabId` to the front of the MRU list.
|
|
708
|
+
*
|
|
709
|
+
* @param {string} tabId
|
|
710
|
+
*/
|
|
711
|
+
function promoteMru(tabId: string) {
|
|
712
|
+
workspace.mruOrder = [tabId, ...workspace.mruOrder.filter((id) => id !== tabId)];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/*
|
|
716
|
+
* ⌃Tab cycling holds a frozen snapshot of the MRU list for the duration of the cycle.
|
|
717
|
+
*
|
|
718
|
+
* Without it, the first ⌃Tab would promote the tab it landed on and the second press would come
|
|
719
|
+
* straight back — the classic "the shortcut only ever toggles between two tabs" bug. The cycle ends
|
|
720
|
+
* at {@link endTabCycle} (the modifier release) or at the next ordinary activation, and only then
|
|
721
|
+
* does the tab the author settled on become the most recent.
|
|
722
|
+
*/
|
|
723
|
+
let _cycleList: string[] | null = null;
|
|
724
|
+
|
|
725
|
+
let _cycleIndex = 0;
|
|
726
|
+
|
|
727
|
+
/** Abandon any in-flight ⌃Tab cycle without promoting anything. */
|
|
728
|
+
function resetTabCycle() {
|
|
729
|
+
_cycleList = null;
|
|
730
|
+
_cycleIndex = 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Point the file tree at the tab the author is now in.
|
|
735
|
+
*
|
|
736
|
+
* The tree and the strip disagreeing about "where am I" is the same class of defect as the tab id
|
|
737
|
+
* disagreeing with its path: two surfaces answering one question two ways.
|
|
738
|
+
*
|
|
739
|
+
* @param {Tab} tab
|
|
740
|
+
*/
|
|
741
|
+
function syncTreeSelection(tab: Tab) {
|
|
742
|
+
if (projectState && tab.documentPath) {
|
|
743
|
+
projectState.selectedPath = tab.documentPath;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Make `tabId` the active tab OF ITS OWN PANE. The one place activation happens.
|
|
749
|
+
*
|
|
750
|
+
* `focus: false` is "put it on screen there, and leave the keyboard where it is" — the shape a
|
|
751
|
+
* side-open needs. It writes `pane.activeTabId` and stops: no `activePaneId` write, no
|
|
752
|
+
* `resetTabCycle`, no `promoteMru`, no `syncTreeSelection`, because every one of those describes
|
|
753
|
+
* where the AUTHOR is and the author has not moved. Drilling into a component with the focus
|
|
754
|
+
* following it is the failure it exists to prevent: the pane the author is typing in changes under
|
|
755
|
+
* them, and their next keystroke edits the definition instead of the page.
|
|
756
|
+
*
|
|
757
|
+
* Legal here and nowhere else — this module is `check-pane-singletons.ts`'s `FOCUS_OWNER`, and the
|
|
758
|
+
* default is still to move the focus, so no existing caller changes behaviour.
|
|
759
|
+
*
|
|
760
|
+
* @param {string} tabId
|
|
761
|
+
* @param {{ cycling?: boolean; focus?: boolean }} [opts] — `cycling` suppresses MRU promotion (see
|
|
762
|
+
* {@link cycleTab}); `focus: false` leaves the keyboard in the pane it is in.
|
|
763
|
+
*/
|
|
764
|
+
function setActiveTab(tabId: string, opts: { cycling?: boolean; focus?: boolean } = {}) {
|
|
765
|
+
const tab = workspace.tabs.get(tabId);
|
|
766
|
+
const pane = paneOfTab(tabId);
|
|
767
|
+
if (!tab || !pane) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
pane.activeTabId = tabId;
|
|
771
|
+
if (opts.focus === false) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
workspace.activePaneId = pane.id;
|
|
775
|
+
if (!opts.cycling) {
|
|
776
|
+
resetTabCycle();
|
|
777
|
+
promoteMru(tabId);
|
|
778
|
+
}
|
|
779
|
+
syncTreeSelection(tab);
|
|
780
|
+
}
|
|
781
|
+
|
|
87
782
|
/**
|
|
88
783
|
* Open a new tab and make it active.
|
|
89
784
|
*
|
|
785
|
+
* Re-opening an id that is already open REPLACES it: the previous tab is disposed and its slot in
|
|
786
|
+
* the strip is reused. It used to be overwritten in the map while its effect scope leaked and a
|
|
787
|
+
* SECOND copy of the id was pushed into `tabOrder`, which is what produced duplicate lit `repeat`
|
|
788
|
+
* keys. Callers that mean "show me this file" should use `files/files.ts`'s `openFileInTab`, which
|
|
789
|
+
* activates the existing tab instead.
|
|
790
|
+
*
|
|
791
|
+
* `preview: true` opens a DISPOSABLE tab (§4.3): single-clicking through the tree or the palette is
|
|
792
|
+
* browsing, and browsing must not litter, so the next preview open in the same pane takes its slot.
|
|
793
|
+
* It is opt-in rather than the default because only the CALLER knows whether the author was
|
|
794
|
+
* browsing or committing — an author who typed a new file name has committed, and silently
|
|
795
|
+
* discarding that tab on their next click is the one failure a preview tab must never have.
|
|
796
|
+
* {@link promoteTab} records a commitment made after the fact.
|
|
797
|
+
*
|
|
798
|
+
* `paneId` says WHERE, and `focus` says whether the keyboard goes with it. Both default to today's
|
|
799
|
+
* answer — the focused pane, and yes — because "open this document" has always meant "in front of
|
|
800
|
+
* me". They exist because drilling into a component, comparing two documents and following a
|
|
801
|
+
* selection all mean the opposite: put it in the pane beside this one and leave me where I am. A
|
|
802
|
+
* `paneId` no pane carries falls back to the focused pane rather than dropping the open, which is
|
|
803
|
+
* the same answer a stale persisted layout gets everywhere else in this module.
|
|
804
|
+
*
|
|
90
805
|
* @param {{
|
|
91
806
|
* id: string;
|
|
92
807
|
* documentPath?: string | null;
|
|
@@ -95,6 +810,10 @@ export function isTabActive(tab: Tab | null): boolean {
|
|
|
95
810
|
* frontmatter?: Record<string, unknown>;
|
|
96
811
|
* sourceFormat?: string | null;
|
|
97
812
|
* capabilities?: { modes?: string[] };
|
|
813
|
+
* openedFrom?: TabOrigin | null;
|
|
814
|
+
* preview?: boolean;
|
|
815
|
+
* paneId?: string;
|
|
816
|
+
* focus?: boolean;
|
|
98
817
|
* }} opts
|
|
99
818
|
* @returns {Tab}
|
|
100
819
|
*/
|
|
@@ -106,17 +825,137 @@ export function openTab(opts: {
|
|
|
106
825
|
frontmatter?: Record<string, unknown>;
|
|
107
826
|
sourceFormat?: string | null;
|
|
108
827
|
capabilities?: { modes?: string[] };
|
|
828
|
+
openedFrom?: TabOrigin | null;
|
|
829
|
+
preview?: boolean;
|
|
830
|
+
paneId?: string;
|
|
831
|
+
focus?: boolean;
|
|
109
832
|
}) {
|
|
110
|
-
const
|
|
833
|
+
const previous = workspace.tabs.get(opts.id);
|
|
834
|
+
const preview = opts.preview === true && previous?.pinned !== true;
|
|
835
|
+
const tab = createTab({ ...opts, preview });
|
|
836
|
+
const pane = (opts.paneId === undefined ? undefined : paneById(opts.paneId)) ?? activePane();
|
|
837
|
+
let slot: number | undefined;
|
|
838
|
+
if (previous) {
|
|
839
|
+
tab.pinned = previous.pinned;
|
|
840
|
+
disposeTab(previous);
|
|
841
|
+
} else {
|
|
842
|
+
/* A preview open takes the pane's existing preview slot rather than adding a chip beside it —
|
|
843
|
+
but only after the sitting tab has been given the chance to become ineligible.
|
|
844
|
+
`promoteDirtyPreviewTabs` is the gate: an edited preview tab stops being replaceable. It
|
|
845
|
+
reads `doc.dirty`, which is a fact a buffer's armed commit has not established yet, so the
|
|
846
|
+
flush has to come BEFORE the victim is chosen. Choosing first and flushing after — which is
|
|
847
|
+
what this did — ran the gate against the state of half a second ago and then destroyed the
|
|
848
|
+
tab the gate would have saved. */
|
|
849
|
+
if (preview) {
|
|
850
|
+
const sitting = previewTabIn(pane);
|
|
851
|
+
const sittingTab = sitting ? workspace.tabs.get(sitting) : null;
|
|
852
|
+
if (sittingTab) {
|
|
853
|
+
void commitTabBuffers(sittingTab);
|
|
854
|
+
}
|
|
855
|
+
promoteDirtyPreviewTabs();
|
|
856
|
+
}
|
|
857
|
+
const replaced = preview ? previewTabIn(pane) : null;
|
|
858
|
+
if (replaced) {
|
|
859
|
+
slot = pane.tabOrder.indexOf(replaced);
|
|
860
|
+
/* THE EIGHTH WAY OUT OF A MONACO BUFFER, and the only one nobody asked for.
|
|
861
|
+
`services/monaco-buffer.ts` lists seven exits: the disposers flush five, and
|
|
862
|
+
`commitTabBuffers` covers ⌘W and quitting. This is the eighth — the author single-clicked
|
|
863
|
+
another page, so the tab they were typing in is destroyed by a gesture that is not a close
|
|
864
|
+
and shows no dialog. Anything the flush above carried has already promoted this tab out of
|
|
865
|
+
the preview slot, so reaching here means the buffer had nothing of the author's in it.
|
|
866
|
+
The flush is not awaited and cannot be: `openTab` is synchronous, its callers hold the
|
|
867
|
+
`Tab` it returns, and `slot` is computed against the strip as it stands. The dock's body
|
|
868
|
+
write is synchronous and therefore counts; the source view's parse is a format round trip
|
|
869
|
+
and does not. That one resolves into a tab that is already gone and its own `tabIsLive`
|
|
870
|
+
re-check drops it — the correct answer to "write into a destroyed tab", and the reason a
|
|
871
|
+
source buffer is the one case this ordering still cannot rescue. */
|
|
872
|
+
closeTab(replaced);
|
|
873
|
+
}
|
|
874
|
+
insertIntoPane(pane, tab.id, slot);
|
|
875
|
+
}
|
|
111
876
|
workspace.tabs.set(tab.id, tab);
|
|
112
|
-
|
|
113
|
-
workspace.activeTabId = tab.id;
|
|
877
|
+
setActiveTab(tab.id, { focus: opts.focus !== false });
|
|
114
878
|
ensureCollab(tab);
|
|
115
879
|
return tab;
|
|
116
880
|
}
|
|
117
881
|
|
|
882
|
+
/** The pane's one preview tab, or null. */
|
|
883
|
+
function previewTabIn(pane: Pane): string | null {
|
|
884
|
+
return pane.tabOrder.find((id) => workspace.tabs.get(id)?.preview === true) ?? null;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Commit to a tab: it stops being replaceable and starts rendering upright.
|
|
889
|
+
*
|
|
890
|
+
* @param {string} tabId
|
|
891
|
+
*/
|
|
892
|
+
export function promoteTab(tabId: string) {
|
|
893
|
+
const tab = workspace.tabs.get(tabId);
|
|
894
|
+
if (tab?.preview === true) {
|
|
895
|
+
tab.preview = false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Promote every preview tab that now has unsaved changes.
|
|
901
|
+
*
|
|
902
|
+
* An edit is the least ambiguous commitment there is, and the alternative — silently discarding
|
|
903
|
+
* someone's typing because they clicked another file — is the one failure a preview tab must never
|
|
904
|
+
* have. Called from the strip's tracking effect, which already reads every tab's `dirty` flag.
|
|
905
|
+
*/
|
|
906
|
+
export function promoteDirtyPreviewTabs() {
|
|
907
|
+
for (const [id, tab] of workspace.tabs) {
|
|
908
|
+
if (tab.preview && tab.doc.dirty) {
|
|
909
|
+
promoteTab(id);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
118
914
|
/**
|
|
119
|
-
*
|
|
915
|
+
* Pin or unpin a tab, moving it to the boundary of the pinned prefix so the head of the strip is
|
|
916
|
+
* always exactly the pinned set.
|
|
917
|
+
*
|
|
918
|
+
* @param {string} tabId
|
|
919
|
+
* @param {boolean} pinned
|
|
920
|
+
*/
|
|
921
|
+
export function setTabPinned(tabId: string, pinned: boolean) {
|
|
922
|
+
const tab = workspace.tabs.get(tabId);
|
|
923
|
+
const pane = paneOfTab(tabId);
|
|
924
|
+
if (!tab || !pane) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
tab.pinned = pinned;
|
|
928
|
+
if (pinned) {
|
|
929
|
+
tab.preview = false;
|
|
930
|
+
}
|
|
931
|
+
const without = pane.tabOrder.filter((id) => id !== tabId);
|
|
932
|
+
const boundary = without.filter((id) => workspace.tabs.get(id)?.pinned === true).length;
|
|
933
|
+
pane.tabOrder = [...without.slice(0, boundary), tabId, ...without.slice(boundary)];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Drag-reorder within a pane. `toIndex` is clamped into the region the tab's pinned state allows,
|
|
938
|
+
* so a drag can never interleave a pinned tab with an unpinned one.
|
|
939
|
+
*
|
|
940
|
+
* @param {string} tabId
|
|
941
|
+
* @param {number} toIndex — the index in the pane's order the tab should end up at
|
|
942
|
+
*/
|
|
943
|
+
export function moveTab(tabId: string, toIndex: number) {
|
|
944
|
+
const tab = workspace.tabs.get(tabId);
|
|
945
|
+
const pane = paneOfTab(tabId);
|
|
946
|
+
if (!tab || !pane) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const without = pane.tabOrder.filter((id) => id !== tabId);
|
|
950
|
+
const boundary = without.filter((id) => workspace.tabs.get(id)?.pinned === true).length;
|
|
951
|
+
const lower = tab.pinned ? 0 : boundary;
|
|
952
|
+
const upper = tab.pinned ? boundary : without.length;
|
|
953
|
+
const at = Math.min(Math.max(toIndex, lower), upper);
|
|
954
|
+
pane.tabOrder = [...without.slice(0, at), tabId, ...without.slice(at)];
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Close a tab and dispose its scope. Activates the most recently used remaining tab.
|
|
120
959
|
*
|
|
121
960
|
* @param {string} tabId
|
|
122
961
|
*/
|
|
@@ -125,25 +964,136 @@ export function closeTab(tabId: string) {
|
|
|
125
964
|
if (!tab) {
|
|
126
965
|
return;
|
|
127
966
|
}
|
|
967
|
+
const pane = paneOfTab(tabId);
|
|
968
|
+
const wasActive = pane?.activeTabId === tabId;
|
|
969
|
+
rememberClosedTab(tab);
|
|
128
970
|
disposeTab(tab);
|
|
129
971
|
workspace.tabs.delete(tabId);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
972
|
+
detachTab(tabId);
|
|
973
|
+
workspace.mruOrder = workspace.mruOrder.filter((id) => id !== tabId);
|
|
974
|
+
resetTabCycle();
|
|
975
|
+
// Emptying EITHER pane collapses the grid: a pane with nothing in it is a hole in it, and the
|
|
976
|
+
// Author asked to close a document, not to keep a slot open for one. `detachTab` above has
|
|
977
|
+
// Usually done this already; this is the belt to its braces, and it goes through the same helper
|
|
978
|
+
// So the primary can never be the pane that leaves.
|
|
979
|
+
if (pane && paneIsEmpty(pane)) {
|
|
980
|
+
closePane(pane.id);
|
|
981
|
+
}
|
|
982
|
+
if (!wasActive) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
// The MRU list, not the strip order: closing a tab should land you on the one you were in before
|
|
986
|
+
// It, which is almost never the rightmost tab in the strip. `detachTab` has already put the
|
|
987
|
+
// Rightmost there as the safe default, so this only ever improves on it.
|
|
988
|
+
const survivor = activePane();
|
|
989
|
+
const next =
|
|
990
|
+
workspace.mruOrder.find((id) => survivor.tabOrder.includes(id)) ??
|
|
991
|
+
survivor.tabOrder.at(-1) ??
|
|
992
|
+
null;
|
|
993
|
+
if (next) {
|
|
994
|
+
setActiveTab(next);
|
|
133
995
|
}
|
|
134
996
|
}
|
|
135
997
|
|
|
136
|
-
/**
|
|
998
|
+
/**
|
|
999
|
+
* Close all open tabs, disposing each. Defers reactivity until fully cleared.
|
|
1000
|
+
*
|
|
1001
|
+
* **Deliberately no gate and no `commitTabBuffers`, unlike `openTab`'s preview replacement and
|
|
1002
|
+
* `requestClose` — and that is now a statement about this function's CALLERS rather than a hole.**
|
|
1003
|
+
* It is the destructive half of a decision somebody else makes: the only caller in `src/` is the
|
|
1004
|
+
* project switch, and `panels/tab-strip.ts`'s `confirmCloseAll` runs both halves of the discipline
|
|
1005
|
+
* for it — commit every tab's buffers, then prompt once for the whole set — before the switch
|
|
1006
|
+
* begins, because everything the switch does after that point is one-way. Putting either half here
|
|
1007
|
+
* would put an `await` and a dialog inside a synchronous teardown that the test harness and
|
|
1008
|
+
* `resetStudioState` also call, which is exactly how a reset would start prompting.
|
|
1009
|
+
*/
|
|
137
1010
|
export function closeAllTabs() {
|
|
138
1011
|
const tabs = [...workspace.tabs.values()];
|
|
139
1012
|
workspace.tabs.clear();
|
|
140
|
-
|
|
141
|
-
workspace.
|
|
1013
|
+
resetPanes();
|
|
1014
|
+
workspace.mruOrder = [];
|
|
1015
|
+
resetTabCycle();
|
|
142
1016
|
for (const tab of tabs) {
|
|
143
1017
|
disposeTab(tab);
|
|
144
1018
|
}
|
|
145
1019
|
}
|
|
146
1020
|
|
|
1021
|
+
/** Back to one empty primary pane — the state the store boots in. */
|
|
1022
|
+
function resetPanes() {
|
|
1023
|
+
workspace.panes = [{ activeTabId: null, derived: null, id: PRIMARY_PANE, tabOrder: [] }];
|
|
1024
|
+
workspace.activePaneId = PRIMARY_PANE;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ─── Reopen closed ────────────────────────────────────────────────────────────
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Record a closing tab so `⌘⇧T` can bring it back.
|
|
1031
|
+
*
|
|
1032
|
+
* Only file-backed tabs are recorded: a virtual grid tab or an untitled document has nothing to
|
|
1033
|
+
* re-read, so offering to reopen it would be a lie.
|
|
1034
|
+
*
|
|
1035
|
+
* @param {Tab} tab
|
|
1036
|
+
*/
|
|
1037
|
+
function rememberClosedTab(tab: Tab) {
|
|
1038
|
+
if (!tab.documentPath) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const path = tab.documentPath;
|
|
1042
|
+
workspace.closedTabs = [
|
|
1043
|
+
{ documentPath: path },
|
|
1044
|
+
...workspace.closedTabs.filter((entry) => entry.documentPath !== path),
|
|
1045
|
+
].slice(0, CLOSED_TAB_LIMIT);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Take the most recently closed document path off the stack.
|
|
1050
|
+
*
|
|
1051
|
+
* Returns the path rather than opening it: reading and parsing a file belongs to `files/files.ts`,
|
|
1052
|
+
* and this module deliberately owns no I/O.
|
|
1053
|
+
*
|
|
1054
|
+
* @returns {string | undefined}
|
|
1055
|
+
*/
|
|
1056
|
+
export function takeClosedTabPath(): string | undefined {
|
|
1057
|
+
const [entry] = workspace.closedTabs;
|
|
1058
|
+
if (!entry) {
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
workspace.closedTabs = workspace.closedTabs.slice(1);
|
|
1062
|
+
return entry.documentPath;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ─── MRU cycling ──────────────────────────────────────────────────────────────
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Walk the MRU list by `step` and activate what it lands on, without reordering it.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {number} step — +1 for `⌃Tab`, -1 for `⌃⇧Tab`
|
|
1071
|
+
* @returns {string | undefined} The id activated, or undefined with fewer than two tabs
|
|
1072
|
+
*/
|
|
1073
|
+
export function cycleTab(step: number): string | undefined {
|
|
1074
|
+
if (!_cycleList) {
|
|
1075
|
+
_cycleList = workspace.mruOrder.filter((id) => workspace.tabs.has(id));
|
|
1076
|
+
_cycleIndex = 0;
|
|
1077
|
+
}
|
|
1078
|
+
const list = _cycleList;
|
|
1079
|
+
if (list.length < 2) {
|
|
1080
|
+
return undefined;
|
|
1081
|
+
}
|
|
1082
|
+
_cycleIndex = (((_cycleIndex + step) % list.length) + list.length) % list.length;
|
|
1083
|
+
const id = list[_cycleIndex]!;
|
|
1084
|
+
setActiveTab(id, { cycling: true });
|
|
1085
|
+
return id;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** End a ⌃Tab cycle — the modifier came up, so the tab the author settled on is now the recent one. */
|
|
1089
|
+
export function endTabCycle() {
|
|
1090
|
+
const settled = workspace.activeTabId;
|
|
1091
|
+
resetTabCycle();
|
|
1092
|
+
if (settled) {
|
|
1093
|
+
promoteMru(settled);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
147
1097
|
/**
|
|
148
1098
|
* Replace all existing tabs with a new one atomically. Ensures activeTab is never null during the
|
|
149
1099
|
* transition.
|
|
@@ -169,8 +1119,11 @@ export function replaceAllTabs(newTabOpts: {
|
|
|
169
1119
|
|
|
170
1120
|
const newTab = createTab(newTabOpts);
|
|
171
1121
|
workspace.tabs.set(newTab.id, newTab);
|
|
172
|
-
|
|
173
|
-
workspace.tabOrder = [newTab.id];
|
|
1122
|
+
resetPanes();
|
|
1123
|
+
workspace.panes[0]!.tabOrder = [newTab.id];
|
|
1124
|
+
workspace.mruOrder = [];
|
|
1125
|
+
resetTabCycle();
|
|
1126
|
+
setActiveTab(newTab.id);
|
|
174
1127
|
ensureCollab(newTab);
|
|
175
1128
|
|
|
176
1129
|
for (const id of oldIds) {
|
|
@@ -189,12 +1142,42 @@ export function replaceAllTabs(newTabOpts: {
|
|
|
189
1142
|
/**
|
|
190
1143
|
* Switch to an existing tab.
|
|
191
1144
|
*
|
|
1145
|
+
* Activation also points the file tree at the tab's document and promotes it in the MRU list — the
|
|
1146
|
+
* strip, the tree and `⌃Tab` all read the same answer to "where am I". With `focus: false` it does
|
|
1147
|
+
* none of those: the tab comes to the front of the pane that owns it and the author stays put.
|
|
1148
|
+
*
|
|
192
1149
|
* @param {string} tabId
|
|
1150
|
+
* @param {{ focus?: boolean }} [opts]
|
|
193
1151
|
*/
|
|
194
|
-
export function activateTab(tabId: string) {
|
|
195
|
-
|
|
196
|
-
|
|
1152
|
+
export function activateTab(tabId: string, opts: { focus?: boolean } = {}) {
|
|
1153
|
+
setActiveTab(tabId, opts);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Move an already-open tab into a pane, and nothing else.
|
|
1158
|
+
*
|
|
1159
|
+
* One tab is one document in one pane's strip (§14.1's ownership half), so this is a MOVE: the tab
|
|
1160
|
+
* leaves whichever pane held it. Nothing is closed, nothing is activated and the focus does not
|
|
1161
|
+
* move — `openFileInTab` decides all three, because only the caller knows whether the author is
|
|
1162
|
+
* committing or browsing.
|
|
1163
|
+
*
|
|
1164
|
+
* Idempotent for a tab already in `paneId`, which is what makes it safe on the follow path.
|
|
1165
|
+
*
|
|
1166
|
+
* @param {string} tabId
|
|
1167
|
+
* @param {string} paneId
|
|
1168
|
+
* @returns {Pane | null} The pane it landed in, or null when either id names nothing.
|
|
1169
|
+
*/
|
|
1170
|
+
export function moveTabToPane(tabId: string, paneId: string): Pane | null {
|
|
1171
|
+
const pane = paneById(paneId);
|
|
1172
|
+
if (!pane || !workspace.tabs.has(tabId)) {
|
|
1173
|
+
return null;
|
|
197
1174
|
}
|
|
1175
|
+
if (pane.tabOrder.includes(tabId)) {
|
|
1176
|
+
return pane;
|
|
1177
|
+
}
|
|
1178
|
+
detachTab(tabId);
|
|
1179
|
+
insertIntoPane(pane, tabId);
|
|
1180
|
+
return pane;
|
|
198
1181
|
}
|
|
199
1182
|
|
|
200
1183
|
/**
|
|
@@ -214,9 +1197,348 @@ export function renameTab(oldId: string, newId: string, newDocumentPath: string)
|
|
|
214
1197
|
tab.documentPath = newDocumentPath;
|
|
215
1198
|
workspace.tabs.delete(oldId);
|
|
216
1199
|
workspace.tabs.set(newId, tab);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
1200
|
+
for (const pane of workspace.panes) {
|
|
1201
|
+
pane.tabOrder = pane.tabOrder.map((id) => (id === oldId ? newId : id));
|
|
1202
|
+
if (pane.activeTabId === oldId) {
|
|
1203
|
+
pane.activeTabId = newId;
|
|
1204
|
+
}
|
|
220
1205
|
}
|
|
1206
|
+
workspace.mruOrder = workspace.mruOrder.map((id) => (id === oldId ? newId : id));
|
|
1207
|
+
resetTabCycle();
|
|
221
1208
|
rekeyCollab(tab);
|
|
222
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
1212
|
+
|
|
1213
|
+
/** What the tab commands need from the rest of Studio. */
|
|
1214
|
+
export interface TabCommandDeps {
|
|
1215
|
+
/** Read a file from disk and show it — `files/files.ts`'s `openFileInTab`. */
|
|
1216
|
+
openFile: (path: string) => void | Promise<void>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Read a file from disk and show it in a NAMED pane, browsing rather than committing and leaving
|
|
1219
|
+
* the keyboard where it is — `files/files.ts`'s `openFileInPane`.
|
|
1220
|
+
*
|
|
1221
|
+
* Injected for the same reason `openFile` is: this module owns the pane model and deliberately
|
|
1222
|
+
* owns no I/O, and `files/files.ts` imports from here.
|
|
1223
|
+
*/
|
|
1224
|
+
openFileInPane: (paneId: string, path: string) => void | Promise<void>;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* The tab-navigation commands, defined next to the model they drive.
|
|
1229
|
+
*
|
|
1230
|
+
* They live here rather than in `commands/defaults.ts` because a capability's record belongs beside
|
|
1231
|
+
* its implementation: the id, the chord and the `run` are one thing, and splitting them across
|
|
1232
|
+
* files is how a second definition site gets born.
|
|
1233
|
+
*
|
|
1234
|
+
* `⌃Tab` carries two chords on purpose. `chordFromEvent` maps the platform's primary modifier to
|
|
1235
|
+
* `mod`, so the SAME physical Ctrl+Tab normalises to `ctrl+tab` on a mac (where ⌘ is `mod` and ⌃
|
|
1236
|
+
* stays `ctrl`) and to `mod+tab` everywhere else. Each spelling is unreachable on the other
|
|
1237
|
+
* platform, so declaring both binds one gesture rather than two.
|
|
1238
|
+
*
|
|
1239
|
+
* @param {CommandRegistry} registry
|
|
1240
|
+
* @param {TabCommandDeps} deps
|
|
1241
|
+
*/
|
|
1242
|
+
export function registerTabCommands(registry: CommandRegistry, deps: TabCommandDeps) {
|
|
1243
|
+
registry.registerAll([...tabCommands(deps), ...paneCommands(deps)]);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* The tab command records. Exported for the CI placement/budget checks and for tests.
|
|
1248
|
+
*
|
|
1249
|
+
* @param {TabCommandDeps} deps
|
|
1250
|
+
* @returns {AnyCommand[]}
|
|
1251
|
+
*/
|
|
1252
|
+
export function tabCommands(deps: TabCommandDeps): AnyCommand[] {
|
|
1253
|
+
// The MRU list is workspace-wide, so cycling is available whenever a second document is open —
|
|
1254
|
+
// In either pane, not just the focused one's strip.
|
|
1255
|
+
const twoOrMoreTabs = () => workspace.tabs.size > 1;
|
|
1256
|
+
return [
|
|
1257
|
+
{
|
|
1258
|
+
id: "document.nextTab",
|
|
1259
|
+
title: "Next Tab",
|
|
1260
|
+
category: "Document",
|
|
1261
|
+
level: "document",
|
|
1262
|
+
keybinding: ["ctrl+tab", "mod+tab"],
|
|
1263
|
+
menus: ["palette"],
|
|
1264
|
+
group: "2_navigate",
|
|
1265
|
+
when: (ctx) => ctx.document.open,
|
|
1266
|
+
enablement: twoOrMoreTabs,
|
|
1267
|
+
requires: "a second open document",
|
|
1268
|
+
run: () => {
|
|
1269
|
+
cycleTab(1);
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
id: "document.previousTab",
|
|
1274
|
+
title: "Previous Tab",
|
|
1275
|
+
category: "Document",
|
|
1276
|
+
level: "document",
|
|
1277
|
+
keybinding: ["ctrl+shift+tab", "mod+shift+tab"],
|
|
1278
|
+
menus: ["palette"],
|
|
1279
|
+
group: "2_navigate",
|
|
1280
|
+
when: (ctx) => ctx.document.open,
|
|
1281
|
+
enablement: twoOrMoreTabs,
|
|
1282
|
+
requires: "a second open document",
|
|
1283
|
+
run: () => {
|
|
1284
|
+
cycleTab(-1);
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
id: "document.reopenClosed",
|
|
1289
|
+
title: "Reopen Closed Document",
|
|
1290
|
+
category: "Document",
|
|
1291
|
+
level: "document",
|
|
1292
|
+
keybinding: "mod+shift+t",
|
|
1293
|
+
menus: ["context/tab", "palette"],
|
|
1294
|
+
group: "1_file",
|
|
1295
|
+
enablement: () => workspace.closedTabs.length > 0,
|
|
1296
|
+
requires: "a document closed in this session",
|
|
1297
|
+
run: async () => {
|
|
1298
|
+
const path = takeClosedTabPath();
|
|
1299
|
+
if (path) {
|
|
1300
|
+
await deps.openFile(path);
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
id: "document.togglePinned",
|
|
1306
|
+
title: "Pin / Unpin Document",
|
|
1307
|
+
category: "Document",
|
|
1308
|
+
level: "document",
|
|
1309
|
+
menus: ["context/tab", "palette"],
|
|
1310
|
+
group: "1_file",
|
|
1311
|
+
when: (ctx) => ctx.document.open,
|
|
1312
|
+
requires: "an open document",
|
|
1313
|
+
run: () => {
|
|
1314
|
+
const id = workspace.activeTabId;
|
|
1315
|
+
const tab = id ? workspace.tabs.get(id) : null;
|
|
1316
|
+
if (id && tab) {
|
|
1317
|
+
setTabPinned(id, !tab.pinned);
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
args: argsSchema({
|
|
1323
|
+
pinned: booleanProperty("True to pin the active document's tab, false to unpin it."),
|
|
1324
|
+
}),
|
|
1325
|
+
id: "document.setPinned",
|
|
1326
|
+
title: "Set Document Pinned",
|
|
1327
|
+
category: "Document",
|
|
1328
|
+
level: "document",
|
|
1329
|
+
menus: ["palette"],
|
|
1330
|
+
group: "1_file",
|
|
1331
|
+
when: (ctx) => ctx.document.open,
|
|
1332
|
+
requires: "an open document",
|
|
1333
|
+
run: (_ctx, args) => {
|
|
1334
|
+
const id = workspace.activeTabId;
|
|
1335
|
+
if (id && workspace.tabs.get(id)) {
|
|
1336
|
+
setTabPinned(id, booleanArg("document.setPinned", args, "pinned"));
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
id: "document.keepOpen",
|
|
1342
|
+
title: "Keep Document Open",
|
|
1343
|
+
category: "Document",
|
|
1344
|
+
level: "document",
|
|
1345
|
+
menus: ["context/tab", "palette"],
|
|
1346
|
+
group: "1_file",
|
|
1347
|
+
when: (ctx) => ctx.document.open,
|
|
1348
|
+
enablement: () => {
|
|
1349
|
+
const id = workspace.activeTabId;
|
|
1350
|
+
return id !== null && workspace.tabs.get(id)?.preview === true;
|
|
1351
|
+
},
|
|
1352
|
+
requires: "a preview document — one opened by a single click",
|
|
1353
|
+
run: () => {
|
|
1354
|
+
const id = workspace.activeTabId;
|
|
1355
|
+
if (id) {
|
|
1356
|
+
promoteTab(id);
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
];
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* The pane commands.
|
|
1365
|
+
*
|
|
1366
|
+
* They live beside the pane model for the same reason the tab commands do, and they are the whole
|
|
1367
|
+
* user-facing surface of §4.1's Pane transport: split, focus, unsplit, zoom.
|
|
1368
|
+
*
|
|
1369
|
+
* `⌘0` and `⌘⌥0` are a PAIR, and they are both bound. `⌘0` was `canvas.zoomReset`, which held it
|
|
1370
|
+
* for the one verb of the zoom cluster that also has a button in the floating zoom pod (§3.2 ⑩) —
|
|
1371
|
+
* so the chord was the second way to reach a control that is already on screen, while focusing a
|
|
1372
|
+
* pane had no way at all. The zoom keeps its pod button and gives up the key.
|
|
1373
|
+
*
|
|
1374
|
+
* @param {TabCommandDeps} deps
|
|
1375
|
+
* @returns {AnyCommand[]}
|
|
1376
|
+
*/
|
|
1377
|
+
export function paneCommands(deps: TabCommandDeps): AnyCommand[] {
|
|
1378
|
+
const twoPanes = () => workspace.panes.length > 1;
|
|
1379
|
+
return [
|
|
1380
|
+
{
|
|
1381
|
+
id: "pane.splitRight",
|
|
1382
|
+
title: "Split Right",
|
|
1383
|
+
category: "View",
|
|
1384
|
+
level: "document",
|
|
1385
|
+
keybinding: "mod+\\",
|
|
1386
|
+
menus: ["context/pane", "context/tab", "palette"],
|
|
1387
|
+
group: "5_pane",
|
|
1388
|
+
when: (ctx) => ctx.document.open,
|
|
1389
|
+
/* One condition, and it is the one the `when` already states: there has to be a document to
|
|
1390
|
+
move. The enablement used to ask `canOpenInSecondPane` — whether the tab declared any of
|
|
1391
|
+
the six kinds the side pane was allowed to host — and refuse a Design-only page outright.
|
|
1392
|
+
Both panes draw a live Canvas now, so the only thing that can refuse a split is having
|
|
1393
|
+
nothing to split. */
|
|
1394
|
+
enablement: () => workspace.activeTabId !== null,
|
|
1395
|
+
// A `requires` string is printed verbatim in every refusal, tooltip and palette row, so it
|
|
1396
|
+
// May only promise what a reader will actually see.
|
|
1397
|
+
requires: "an open document",
|
|
1398
|
+
undo: "none",
|
|
1399
|
+
run: () => {
|
|
1400
|
+
splitRight();
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
args: argsSchema({
|
|
1405
|
+
path: stringProperty("Project-relative path of the document to open beside this one."),
|
|
1406
|
+
}),
|
|
1407
|
+
id: "pane.compareWith",
|
|
1408
|
+
title: "Compare With…",
|
|
1409
|
+
category: "View",
|
|
1410
|
+
level: "document",
|
|
1411
|
+
menus: ["context/pane", "context/tab", "palette"],
|
|
1412
|
+
group: "5_pane",
|
|
1413
|
+
when: (ctx) => ctx.document.open,
|
|
1414
|
+
/* This is NOT `pane.derive`, and the distinction is the whole reason both exist. `derive`
|
|
1415
|
+
shows a projection OF THIS PANE — its Code, its diff, the same page at another breakpoint —
|
|
1416
|
+
and follows it. `compareWith` puts THAT DOCUMENT beside this one and then leaves it alone.
|
|
1417
|
+
Collapsing them would give "open a second document in the other pane" two definition
|
|
1418
|
+
sites, which is how the two names in §4.1 came to disagree in the first place. */
|
|
1419
|
+
enablement: () => workspace.activeTabId !== null,
|
|
1420
|
+
requires: "an open document",
|
|
1421
|
+
undo: "none",
|
|
1422
|
+
aiTool: {
|
|
1423
|
+
description:
|
|
1424
|
+
"Open a second document in the pane beside the current one, without moving the document " +
|
|
1425
|
+
"you are in.",
|
|
1426
|
+
name: "compare_with",
|
|
1427
|
+
},
|
|
1428
|
+
run: async (_ctx, args) => {
|
|
1429
|
+
const path = stringArg("pane.compareWith", args, "path");
|
|
1430
|
+
const here = activePane();
|
|
1431
|
+
const held = here.activeTabId ? workspace.tabs.get(here.activeTabId) : null;
|
|
1432
|
+
/* Refused by NAME rather than by `requires`, which is one sentence for the whole command
|
|
1433
|
+
and cannot express "in the other pane". Comparing a document with itself is not a
|
|
1434
|
+
refusal the enablement can see: it depends on the argument. */
|
|
1435
|
+
if (held?.documentPath === path) {
|
|
1436
|
+
throw new RangeError(
|
|
1437
|
+
`command "pane.compareWith" argument "path": "${path}" is the document this pane is ` +
|
|
1438
|
+
`already showing — name a different one`,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
/* Nothing closes, nothing unsplits, and the FOCUS DOES NOT MOVE: comparing is a read.
|
|
1442
|
+
{@link receivingPane} rather than `sidePane`, because the pane beside this one may be a
|
|
1443
|
+
LENS — and a lens owns no tab, so the document landed in a `tabOrder` that `tabOfPane`
|
|
1444
|
+
hops straight past. Nothing was on screen, nothing was in a strip, and
|
|
1445
|
+
`workspace.activeTabId` went on reporting the source. */
|
|
1446
|
+
await deps.openFileInPane(receivingPane().id, path);
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
id: "pane.focusPrimary",
|
|
1451
|
+
title: "Focus Primary Pane",
|
|
1452
|
+
category: "View",
|
|
1453
|
+
level: "document",
|
|
1454
|
+
keybinding: "mod+0",
|
|
1455
|
+
menus: ["palette"],
|
|
1456
|
+
group: "5_pane",
|
|
1457
|
+
enablement: twoPanes,
|
|
1458
|
+
requires: "a split pane grid",
|
|
1459
|
+
undo: "none",
|
|
1460
|
+
run: () => {
|
|
1461
|
+
focusPane(PRIMARY_PANE);
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
id: "pane.focusSecondary",
|
|
1466
|
+
title: "Focus Side Pane",
|
|
1467
|
+
category: "View",
|
|
1468
|
+
level: "document",
|
|
1469
|
+
keybinding: "mod+alt+0",
|
|
1470
|
+
menus: ["palette"],
|
|
1471
|
+
group: "5_pane",
|
|
1472
|
+
enablement: twoPanes,
|
|
1473
|
+
requires: "a split pane grid",
|
|
1474
|
+
undo: "none",
|
|
1475
|
+
run: () => {
|
|
1476
|
+
focusPane(SECONDARY_PANE);
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: "pane.unsplit",
|
|
1481
|
+
/* "Unsplit" and "a split pane grid" were true while a second pane could only arrive by
|
|
1482
|
+
splitting. A derived pane is not a split — nothing moved into it — and it is also the only
|
|
1483
|
+
exit a LENS has, so the record has to name what it does rather than what undoes the one
|
|
1484
|
+
gesture that used to make it. The id is unchanged: the manifest addresses the id. */
|
|
1485
|
+
title: "Close Side Pane",
|
|
1486
|
+
category: "View",
|
|
1487
|
+
level: "document",
|
|
1488
|
+
menus: ["context/pane", "palette"],
|
|
1489
|
+
group: "5_pane",
|
|
1490
|
+
enablement: twoPanes,
|
|
1491
|
+
requires: "a second pane",
|
|
1492
|
+
undo: "none",
|
|
1493
|
+
run: runUnsplit,
|
|
1494
|
+
},
|
|
1495
|
+
];
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* `pane.unsplit`'s subject: **the DERIVED pane when the grid has one**, else the side pane.
|
|
1500
|
+
*
|
|
1501
|
+
* The subject used to be `activePaneId === PRIMARY ? SECONDARY : activePaneId` — "the pane beside
|
|
1502
|
+
* me, unless I am in it" — a complete answer while a second pane could only arrive by splitting and
|
|
1503
|
+
* both panes were the same kind of thing. A derived pane is not: it is subordinate to the pane it
|
|
1504
|
+
* projects, it is mintable in EITHER direction (derive from the secondary and the PRIMARY becomes
|
|
1505
|
+
* the projection), and two new surfaces offer this command as _that pane's own exit_ — the
|
|
1506
|
+
* derivation chip's ✕ in `panels/tab-strip.ts` and the `Close Side Pane` row of its own preset
|
|
1507
|
+
* menu. Both focus their pane before running, so with a derived PRIMARY the old expression read the
|
|
1508
|
+
* focus, saw `PRIMARY`, and closed the pane holding the author's work:
|
|
1509
|
+
*
|
|
1510
|
+
* before: primary[pages/index.json]*derived secondary[scratch.json] focus=secondary
|
|
1511
|
+
* after: primary[pages/index.json, scratch.json] focus=primary
|
|
1512
|
+
*
|
|
1513
|
+
* Scanning for the derivation is the same shape `pane-derive.ts`'s `pinnablePane` uses, and for the
|
|
1514
|
+
* same reason: a grid holds at most one derived pane, so the answer reads no focus and is therefore
|
|
1515
|
+
* the same from the palette, from the chip and from either pane's menu.
|
|
1516
|
+
*
|
|
1517
|
+
* **One title, two acts, because for every pane that CAN leave the grid they are the same act.**
|
|
1518
|
+
* Closing a projection's pane ends the projection. {@link closePane} refuses to remove the PRIMARY
|
|
1519
|
+
* — it is `panes[0]`, the id `resolveRegion("pane")` canonicalises onto and nine screenshots crop —
|
|
1520
|
+
* so for a derived primary only the second half is available: the derivation is dropped and the
|
|
1521
|
+
* pane keeps whatever it owns. Passing `PRIMARY_PANE` to `closePane` would not have done that; it
|
|
1522
|
+
* redirects to "collapse the other pane instead", which is how pressing the projection's ✕ closed
|
|
1523
|
+
* the author's second pane and left them looking at the projection's document.
|
|
1524
|
+
*
|
|
1525
|
+
* A LENS owns no tab, so ending its derivation leaves a pane with no subject — §18.1 rule 3 — and
|
|
1526
|
+
* the repair for an empty primary is exactly that redirect. That case therefore still collapses,
|
|
1527
|
+
* which is right: unsplitting a Code lens should leave you with the document.
|
|
1528
|
+
*/
|
|
1529
|
+
function runUnsplit(): void {
|
|
1530
|
+
const derived = workspace.panes.find((pane) => pane.derived !== null);
|
|
1531
|
+
if (derived?.id === PRIMARY_PANE) {
|
|
1532
|
+
derived.derived = null;
|
|
1533
|
+
if (!paneIsEmpty(derived)) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/* `SECONDARY_PANE`, flatly, where the fallback used to read
|
|
1538
|
+
`activePaneId === PRIMARY ? SECONDARY : activePaneId`. With {@link MAX_PANES} at 2 and both ids
|
|
1539
|
+
minted by {@link addPane} from the two constants, `activePaneId` is one of those two and BOTH
|
|
1540
|
+
branches of that ternary evaluate to `SECONDARY_PANE` — a choice with one outcome, which no
|
|
1541
|
+
test could tell from its opposite. It is a fossil of the pre-derivation subject, described in
|
|
1542
|
+
the docstring above; the sentence it belonged to is the one that has to stay. */
|
|
1543
|
+
closePane(derived?.id ?? SECONDARY_PANE);
|
|
1544
|
+
}
|