@jxsuite/studio 2.0.0 → 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-0hzentwq.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-0hzentwq.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-2c188n41.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-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-97qcgvdn.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-hhj92gcr.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-ygzav7z5.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-n9zmpxjg.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 +23711 -35185
- 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 +34 -9
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +270 -262
- 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 -120
- 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 +75 -2
- 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 +113 -0
- 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-qct0gt8f.js +0 -531
- package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
- package/dist/chunks/files-02zn5y24.js +0 -50
- package/dist/chunks/files-02zn5y24.js.map +0 -9
- package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
- package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
- package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
- package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
- package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
- package/dist/chunks/settings-modal-gj0hrah4.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-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-568nx4t5.js +0 -469
- package/dist/chunks/studio-568nx4t5.js.map +0 -14
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-asvf0j9e.js +0 -429
- package/dist/chunks/studio-asvf0j9e.js.map +0 -13
- package/dist/chunks/studio-d77qm3pk.js +0 -3427
- package/dist/chunks/studio-d77qm3pk.js.map +0 -26
- package/dist/chunks/studio-j6gqyjf4.js +0 -1854
- package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
- package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-rq0vxb19.js +0 -3744
- package/dist/chunks/studio-rq0vxb19.js.map +0 -22
- package/dist/chunks/studio-wd6ewdvd.js +0 -853
- package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
- 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-ygzav7z5.js.map +0 -46
- package/dist/chunks/studio-zmy7241e.js +0 -399
- package/dist/chunks/studio-zmy7241e.js.map +0 -12
- 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-2c188n41.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-hhj92gcr.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
package/src/files/files.ts
CHANGED
|
@@ -7,22 +7,35 @@
|
|
|
7
7
|
* file-ops.js. Every name the user supplies (new file, rename) is collected with the Spectrum
|
|
8
8
|
* prompt dialog from ui/layers.ts — never a native browser prompt (studio-ui-guidelines.md §8.7).
|
|
9
9
|
*
|
|
10
|
+
* **The tree is a flat list of rows, and the DOM holds a window onto it** ({@link FileRow},
|
|
11
|
+
* `ui/virtual-window.ts`). It used to recurse a template per directory level, which is why it drew
|
|
12
|
+
* every expanded row of every expanded directory — a `node_modules` expanded by accident is tens of
|
|
13
|
+
* thousands of `sp-icon` custom elements, built synchronously, on every repaint. A recursion has no
|
|
14
|
+
* row list to window, so the recursion moved into {@link collectFileRows}, which produces the rows
|
|
15
|
+
* in display order and nothing else; the render is then a window over that array.
|
|
16
|
+
*
|
|
17
|
+
* Flattening costs the `role="group"` wrappers, and pays for them with `aria-level` +
|
|
18
|
+
* `aria-posinset` / `aria-setsize` on every row — the same shape the Outline has always had, and
|
|
19
|
+
* the only shape that stays TRUE when the tree draws eleven rows out of ten thousand.
|
|
20
|
+
*
|
|
10
21
|
* @docs studio/interface
|
|
11
22
|
*/
|
|
12
23
|
|
|
13
24
|
import { html, nothing } from "lit-html";
|
|
14
25
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
26
|
+
import { localeLabel, localeOfPath, resolveI18n } from "@jxsuite/schema/locale";
|
|
15
27
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
16
28
|
import { ref } from "lit-html/directives/ref.js";
|
|
17
|
-
import { renderPopover,
|
|
18
|
-
import {
|
|
29
|
+
import { renderPopover, showPromptDialog } from "../ui/layers";
|
|
30
|
+
import { projectState, requireProjectState, setProjectState } from "../store";
|
|
19
31
|
import { getPlatform } from "../platform";
|
|
20
|
-
import {
|
|
32
|
+
import { notify } from "../services/notify";
|
|
21
33
|
import { loadComponentRegistry } from "./components";
|
|
22
34
|
import { ensureDependenciesInstalled } from "../packages/ensure-deps";
|
|
23
35
|
import { maybePromptJxsuiteUpdate } from "../packages/jxsuite-update";
|
|
24
36
|
import { autoSyncProjectOnOpen } from "../packages/pull-package-sync";
|
|
25
37
|
import { markLocalMutation } from "./fs-events";
|
|
38
|
+
import { registerPanel } from "../panels/panel-registry";
|
|
26
39
|
import { UPLOAD_ACCEPT, isImage, uploadAssets } from "./media-upload";
|
|
27
40
|
import { isCollabPath } from "../collab/collab-state";
|
|
28
41
|
import {
|
|
@@ -33,15 +46,21 @@ import {
|
|
|
33
46
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
34
47
|
import {
|
|
35
48
|
activateTab,
|
|
49
|
+
moveTabToPane,
|
|
36
50
|
openTab,
|
|
51
|
+
paneOfTab,
|
|
52
|
+
receivingPane,
|
|
37
53
|
renameTab,
|
|
38
54
|
replaceAllTabs,
|
|
39
55
|
setWorkspaceProject,
|
|
40
56
|
workspace,
|
|
41
57
|
} from "../workspace/workspace";
|
|
42
|
-
import {
|
|
58
|
+
import { openCsvGridTab, openPagesGrid } from "../grid/grid-open";
|
|
43
59
|
import { collectionDirs } from "../grid/sources/content-source";
|
|
44
|
-
import {
|
|
60
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
61
|
+
import { collectionOfPath } from "../content/entry-model";
|
|
62
|
+
import { confirmFileDelete, parseSourceForPath, renamePromptMessage } from "./file-ops";
|
|
63
|
+
import { invalidateUsages } from "../services/references";
|
|
45
64
|
import {
|
|
46
65
|
documentExtensions,
|
|
47
66
|
formatForPath,
|
|
@@ -50,14 +69,23 @@ import {
|
|
|
50
69
|
refreshExtensionUi,
|
|
51
70
|
refreshFormats,
|
|
52
71
|
} from "../format/format-host";
|
|
53
|
-
import {
|
|
72
|
+
import { markSessionRestored, persistedSession, resetProjectShell, setActivityTab } from "../shell";
|
|
73
|
+
import { restoreSession } from "../workspace/session";
|
|
74
|
+
import { cleanupGitPanel } from "../panels/git-panel";
|
|
54
75
|
import { addRecentProject, trackRecentFile } from "../recent-projects";
|
|
76
|
+
import {
|
|
77
|
+
listWindow,
|
|
78
|
+
measuredRowHeight,
|
|
79
|
+
revealListRow,
|
|
80
|
+
watchListWindow,
|
|
81
|
+
} from "../ui/virtual-window";
|
|
55
82
|
import type { TemplateResult } from "lit-html";
|
|
56
83
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
57
|
-
import type {
|
|
58
|
-
import type { Tab } from "../tabs/tab.js";
|
|
84
|
+
import type { ResolvedI18n } from "@jxsuite/schema/locale";
|
|
59
85
|
import type { DirEntry, RenameResult } from "../types";
|
|
86
|
+
import type { ListWindowWatch } from "../ui/virtual-window";
|
|
60
87
|
import { rectOf } from "../utils/geometry";
|
|
88
|
+
import { repeat } from "lit-html/directives/repeat.js";
|
|
61
89
|
|
|
62
90
|
// ─── File icon map ────────────────────────────────────────────────────────────
|
|
63
91
|
|
|
@@ -123,7 +151,7 @@ export async function loadProject() {
|
|
|
123
151
|
await ensureDependenciesInstalled();
|
|
124
152
|
await loadDirectory(".");
|
|
125
153
|
await loadComponentRegistry();
|
|
126
|
-
await
|
|
154
|
+
await openLastSessionOrHome();
|
|
127
155
|
void maybePromptJxsuiteUpdate(meta.root);
|
|
128
156
|
}
|
|
129
157
|
// If not a site project (monorepo) — show welcome prompt, don't load tree
|
|
@@ -135,25 +163,25 @@ export async function loadProject() {
|
|
|
135
163
|
// ─── Open Project (PAL-based) ─────────────────────────────────────────────
|
|
136
164
|
|
|
137
165
|
/**
|
|
138
|
-
* Open a project via the platform adapter.
|
|
166
|
+
* Open a project via the platform adapter, into THIS window.
|
|
139
167
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
168
|
+
* Reports whether a project was actually opened: a cancelled picker and a failed open both leave
|
|
169
|
+
* the window on the project it already had, and the caller announces the outcome — "Opening the
|
|
170
|
+
* project…" over a dialog the user just dismissed is a report of something that did not happen.
|
|
171
|
+
*
|
|
172
|
+
* @param {{ renderLeftPanel: () => void }} ctx
|
|
173
|
+
* @returns {Promise<boolean>} Whether the window changed project.
|
|
144
174
|
*/
|
|
145
175
|
export async function openProject({
|
|
146
|
-
renderActivityBar,
|
|
147
176
|
renderLeftPanel,
|
|
148
177
|
}: {
|
|
149
|
-
renderActivityBar: () => void;
|
|
150
178
|
renderLeftPanel: () => void;
|
|
151
|
-
}) {
|
|
179
|
+
}): Promise<boolean> {
|
|
152
180
|
try {
|
|
153
181
|
const platform = getPlatform();
|
|
154
182
|
const result = await platform.openProject();
|
|
155
183
|
if (!result) {
|
|
156
|
-
return;
|
|
184
|
+
return false;
|
|
157
185
|
} // User cancelled
|
|
158
186
|
|
|
159
187
|
const { config, handle } = result;
|
|
@@ -206,16 +234,63 @@ export async function openProject({
|
|
|
206
234
|
}
|
|
207
235
|
requireProjectState().projectDirs = foundDirs;
|
|
208
236
|
|
|
209
|
-
|
|
237
|
+
// Source control, the stylebook selection and the settings tab describe the project being
|
|
238
|
+
// Left behind. The poll timer goes with them; the panel re-arms it on its next render.
|
|
239
|
+
cleanupGitPanel();
|
|
240
|
+
resetProjectShell();
|
|
241
|
+
setActivityTab("files");
|
|
210
242
|
addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
|
|
211
|
-
renderActivityBar();
|
|
212
243
|
renderLeftPanel();
|
|
213
|
-
|
|
244
|
+
// The project's name is permanent state in the status bar's PROJECT field now.
|
|
214
245
|
|
|
215
|
-
await
|
|
246
|
+
await openLastSessionOrHome();
|
|
216
247
|
void maybePromptJxsuiteUpdate(requireProjectState().projectRoot);
|
|
248
|
+
return true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
notify.error("Could not open the project.", {
|
|
251
|
+
detail: errorMessage(error),
|
|
252
|
+
source: "Open Project",
|
|
253
|
+
});
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Give a freshly created project a git repository, so its first irreversible action is recoverable.
|
|
260
|
+
*
|
|
261
|
+
* Runs on the create/import path only, immediately after the backend has written the project — the
|
|
262
|
+
* scaffold is not a repository and nothing else in Studio says so, while Delete and Rename are one
|
|
263
|
+
* confirm click away. `activate` binds the backend to the new root first, because `gitInit` takes
|
|
264
|
+
* no argument and would otherwise run against whichever project the window was serving.
|
|
265
|
+
*
|
|
266
|
+
* Never re-initialises, and never runs on repository-backed platforms (`createDestination: "repo"`
|
|
267
|
+
* — a cloud project _is_ a GitHub repository, and its `gitInit` is a no-op by design).
|
|
268
|
+
*
|
|
269
|
+
* @param {string} root Absolute root of the project just created.
|
|
270
|
+
* @returns {Promise<boolean>} Whether a repository was initialised.
|
|
271
|
+
*/
|
|
272
|
+
export async function initProjectRepo(root: string): Promise<boolean> {
|
|
273
|
+
const platform = getPlatform();
|
|
274
|
+
if (platform.createDestination !== "path") {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
await platform.activate(root);
|
|
279
|
+
const status = await platform.gitStatus();
|
|
280
|
+
if (status.isRepo) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
await platform.gitInit();
|
|
284
|
+
notify.success("Initialized a git repository for this project.");
|
|
285
|
+
return true;
|
|
217
286
|
} catch (error) {
|
|
218
|
-
|
|
287
|
+
// Version control is a safety net, not a precondition — a project that was written stays
|
|
288
|
+
// Written. Say so rather than failing the create the user just completed.
|
|
289
|
+
notify.warn("Could not initialize a git repository — the project itself was written.", {
|
|
290
|
+
detail: errorMessage(error),
|
|
291
|
+
source: "Source Control",
|
|
292
|
+
});
|
|
293
|
+
return false;
|
|
219
294
|
}
|
|
220
295
|
}
|
|
221
296
|
|
|
@@ -249,6 +324,51 @@ export async function openHomePage() {
|
|
|
249
324
|
}
|
|
250
325
|
}
|
|
251
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Reopen the documents this project was last left with, or its home page if there are none.
|
|
329
|
+
*
|
|
330
|
+
* Plan §4.4, and P3's "Newly possible": **the session survives a relaunch.** Every one of the three
|
|
331
|
+
* ways into a project — the `?project=` bootstrap, the PAL picker and a recent-project open — ended
|
|
332
|
+
* at `openHomePage()`, so nine open documents, a split and the mode you were in were lost each
|
|
333
|
+
* time. The per-project record's own interface said "session state grows into this shape".
|
|
334
|
+
*
|
|
335
|
+
* ONE function for all three, because the three used to be three calls to `openHomePage` and this
|
|
336
|
+
* is exactly the kind of behaviour that lands on two of them.
|
|
337
|
+
*
|
|
338
|
+
* A path that no longer resolves is skipped, and a session that restores NOTHING falls through to
|
|
339
|
+
* the home page rather than leaving an empty window: files move, and a stale record must not cost
|
|
340
|
+
* you the one page a project can always show.
|
|
341
|
+
*
|
|
342
|
+
* @returns Whether a SESSION was restored — `false` means the home page was opened instead.
|
|
343
|
+
*/
|
|
344
|
+
export async function openLastSessionOrHome(): Promise<boolean> {
|
|
345
|
+
// `workspace.projectRoot`, and NOT a root passed in: that is the key `persistProjectShell` writes
|
|
346
|
+
// Under, and the three callers each know the project by a slightly different name — `meta.root`,
|
|
347
|
+
// `projectState.projectRoot`, the recent-list entry. One reader of one field cannot disagree with
|
|
348
|
+
// The writer; three callers passing three spellings silently restore nothing.
|
|
349
|
+
const session = persistedSession(workspace.projectRoot);
|
|
350
|
+
// Read first, THEN allow writes: the persist effect fires the moment `workspace.projectRoot` is
|
|
351
|
+
// Set, and an empty workspace captured at that instant would overwrite the very record this line
|
|
352
|
+
// Reads. See `markSessionRestored`.
|
|
353
|
+
markSessionRestored(workspace.projectRoot);
|
|
354
|
+
if (session) {
|
|
355
|
+
const opened = await restoreSession(session, {
|
|
356
|
+
ensureSecondPane: () => {
|
|
357
|
+
receivingPane();
|
|
358
|
+
},
|
|
359
|
+
openFile: (path, paneId) => openFileInTab(path, { focus: false, paneId }),
|
|
360
|
+
});
|
|
361
|
+
if (opened > 0) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
await openHomePage();
|
|
366
|
+
// Whether a SESSION was restored, which is not the same as whether anything opened: the
|
|
367
|
+
// `?project=` boot needs to know if it should still run its own inline open, and a home page is
|
|
368
|
+
// Not an answer to that question.
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
252
372
|
// ─── File tree templates ──────────────────────────────────────────────────────
|
|
253
373
|
|
|
254
374
|
function fileTypeIconTpl(name: string, type: string) {
|
|
@@ -364,27 +484,113 @@ export function renderFilesTemplate({
|
|
|
364
484
|
@submit=${(e: Event) => e.preventDefault()}
|
|
365
485
|
></sp-search>
|
|
366
486
|
</div>
|
|
367
|
-
<div
|
|
368
|
-
|
|
487
|
+
<div
|
|
488
|
+
class="file-tree"
|
|
489
|
+
role="tree"
|
|
490
|
+
aria-label="Project files"
|
|
491
|
+
${ref((el) => {
|
|
492
|
+
if (el) {
|
|
493
|
+
afterFileTreeRender(el as HTMLElement);
|
|
494
|
+
}
|
|
495
|
+
})}
|
|
496
|
+
>
|
|
497
|
+
${fileTreeBodyTemplate({ openFileFn, renderLeftPanel })}
|
|
369
498
|
</div>
|
|
370
499
|
`;
|
|
371
500
|
}
|
|
372
501
|
|
|
373
|
-
|
|
374
|
-
|
|
502
|
+
// ─── The row model, and the window onto it ───────────────────────────────────
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* One row the Files tree would draw, in display order.
|
|
506
|
+
*
|
|
507
|
+
* An expanded directory contributes its children immediately after its own row, so an index into
|
|
508
|
+
* this array is what "the row below this one" MEANS for the keyboard — and it goes on meaning it
|
|
509
|
+
* whether or not the row below happens to be painted.
|
|
510
|
+
*/
|
|
511
|
+
interface FileRow {
|
|
512
|
+
/**
|
|
513
|
+
* Lit's `repeat` key.
|
|
514
|
+
*
|
|
515
|
+
* Keyed, where the recursive form was positional: a windowed list re-uses its DOM nodes for
|
|
516
|
+
* DIFFERENT rows as the window slides, so positional reuse would leave the keyboard focused on an
|
|
517
|
+
* element that has since become another file. The prefix is what keeps a directory's own row and
|
|
518
|
+
* the "Loading…" row underneath it apart — they name the same path.
|
|
519
|
+
*/
|
|
520
|
+
key: string;
|
|
521
|
+
path: string;
|
|
522
|
+
name: string;
|
|
523
|
+
type: string;
|
|
524
|
+
depth: number;
|
|
525
|
+
expanded: boolean;
|
|
526
|
+
/** A placeholder for a directory whose listing has not arrived yet. Not a `treeitem`. */
|
|
527
|
+
loading: boolean;
|
|
528
|
+
/** 1-based position among this row's siblings (`aria-posinset`). */
|
|
529
|
+
posInSet: number;
|
|
530
|
+
/** How many siblings the row has, itself included (`aria-setsize`). */
|
|
531
|
+
setSize: number;
|
|
532
|
+
/**
|
|
533
|
+
* The declared locale whose directory this row sits under, or absent.
|
|
534
|
+
*
|
|
535
|
+
* A chip, not a filter term: the tree's search still matches `entry.name` alone, because a query
|
|
536
|
+
* that silently also matched a language would make "why is this file here" unanswerable from what
|
|
537
|
+
* is on screen.
|
|
538
|
+
*/
|
|
539
|
+
locale?: string | undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The declared height of one row — `styles/panels.css` `.file-tree-item { block-size: 24px }`.
|
|
544
|
+
*
|
|
545
|
+
* The first paint of a session windows by this constant, because nothing has been laid out yet to
|
|
546
|
+
* measure; {@link fileRowHeight} measures a real row from then on and believes the measurement.
|
|
547
|
+
*/
|
|
548
|
+
export const FILE_ROW_HEIGHT = 24;
|
|
549
|
+
|
|
550
|
+
/** The rows the tree last built, in display order. */
|
|
551
|
+
let _fileRows: FileRow[] = [];
|
|
552
|
+
/** The `.file-tree` element, kept between renders so the next one can be windowed. */
|
|
553
|
+
let _fileList: HTMLElement | null = null;
|
|
554
|
+
/** The scroll watch that repaints the tree as its scroller moves. */
|
|
555
|
+
let _fileWatch: ListWindowWatch | null = null;
|
|
556
|
+
/** The Navigator repaint, captured per render so the scroll watch never holds a stale one. */
|
|
557
|
+
let _filesRerender: (() => void) | null = null;
|
|
558
|
+
/** A keyboard jump that had to scroll first, spent by the repaint it provoked. */
|
|
559
|
+
let _pendingFocusPath: string | null = null;
|
|
560
|
+
|
|
561
|
+
/** The height one row actually has; the declared constant until a row has been laid out. */
|
|
562
|
+
function fileRowHeight(): number {
|
|
563
|
+
return measuredRowHeight(_fileList, ".file-tree-item", FILE_ROW_HEIGHT);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Append one directory's rows, and every expanded child directory's rows after their own row.
|
|
568
|
+
*
|
|
569
|
+
* The listing side effect stays exactly where the recursive template had it: a directory nobody has
|
|
570
|
+
* listed yet is fetched, and a placeholder holds its place until the repaint arrives.
|
|
571
|
+
*/
|
|
572
|
+
function collectFileRows(
|
|
375
573
|
dirPath: string,
|
|
376
574
|
depth: number,
|
|
377
|
-
|
|
378
|
-
|
|
575
|
+
rows: FileRow[],
|
|
576
|
+
ctx: { renderLeftPanel: () => void },
|
|
577
|
+
i18n: ResolvedI18n | null,
|
|
578
|
+
): void {
|
|
379
579
|
const entries = requireProjectState().dirs.get(dirPath);
|
|
380
580
|
if (!entries) {
|
|
381
581
|
void loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
582
|
+
rows.push({
|
|
583
|
+
depth,
|
|
584
|
+
expanded: false,
|
|
585
|
+
key: `loading:${dirPath}`,
|
|
586
|
+
loading: true,
|
|
587
|
+
name: "Loading…",
|
|
588
|
+
path: dirPath,
|
|
589
|
+
posInSet: 1,
|
|
590
|
+
setSize: 1,
|
|
591
|
+
type: "file",
|
|
592
|
+
});
|
|
593
|
+
return;
|
|
388
594
|
}
|
|
389
595
|
|
|
390
596
|
const sorted = [...entries].toSorted((a, b) => {
|
|
@@ -402,126 +608,320 @@ function renderTreeLevelTemplate(
|
|
|
402
608
|
? sorted.filter((e) => e.type === "directory" || e.name.toLowerCase().includes(query))
|
|
403
609
|
: sorted;
|
|
404
610
|
|
|
405
|
-
|
|
611
|
+
for (const [index, entry] of filtered.entries()) {
|
|
406
612
|
const isDir = entry.type === "directory";
|
|
407
|
-
const isExpanded = requireProjectState().expanded.has(entry.path);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
requireProjectState().expanded.delete(entry.path);
|
|
425
|
-
} else {
|
|
426
|
-
requireProjectState().expanded.add(entry.path);
|
|
427
|
-
if (!requireProjectState().dirs.has(entry.path)) {
|
|
428
|
-
await loadDirectory(entry.path);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
ctx.renderLeftPanel();
|
|
432
|
-
} else {
|
|
433
|
-
ctx.openFileFn(entry.path);
|
|
434
|
-
}
|
|
435
|
-
}}
|
|
436
|
-
@contextmenu=${(e: MouseEvent) => {
|
|
437
|
-
e.preventDefault();
|
|
438
|
-
e.stopPropagation();
|
|
439
|
-
showFileContextMenu(e, entry, ctx);
|
|
440
|
-
}}
|
|
441
|
-
>
|
|
442
|
-
${
|
|
443
|
-
isDir
|
|
444
|
-
? html`<span class="file-tree-toggle">${isExpanded ? "\u25BC" : "\u25B6"}</span>`
|
|
445
|
-
: html`<span class="file-tree-toggle empty"> </span>`
|
|
446
|
-
}
|
|
447
|
-
<span class="file-tree-icon">${fileTypeIconTpl(entry.path, entry.type)}</span>
|
|
448
|
-
<span class="file-tree-name">${entry.name}</span>
|
|
449
|
-
</div>
|
|
450
|
-
${
|
|
451
|
-
isDir && isExpanded
|
|
452
|
-
? html`<div role="group">${renderTreeLevelTemplate(entry.path, depth + 1, ctx)}</div>`
|
|
453
|
-
: nothing
|
|
454
|
-
}
|
|
455
|
-
`;
|
|
456
|
-
});
|
|
613
|
+
const isExpanded = isDir && requireProjectState().expanded.has(entry.path);
|
|
614
|
+
rows.push({
|
|
615
|
+
depth,
|
|
616
|
+
expanded: isExpanded,
|
|
617
|
+
key: entry.path,
|
|
618
|
+
loading: false,
|
|
619
|
+
name: entry.name,
|
|
620
|
+
path: entry.path,
|
|
621
|
+
locale: localeOfPath(entry.path, i18n) ?? undefined,
|
|
622
|
+
posInSet: index + 1,
|
|
623
|
+
setSize: filtered.length,
|
|
624
|
+
type: entry.type,
|
|
625
|
+
});
|
|
626
|
+
if (isExpanded) {
|
|
627
|
+
collectFileRows(entry.path, depth + 1, rows, ctx, i18n);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
457
630
|
}
|
|
458
631
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
632
|
+
/** The model index of `path`, or -1. A "Loading…" row is keyed apart and never matches. */
|
|
633
|
+
function fileIndexOfPath(path: string | undefined): number {
|
|
634
|
+
return path === undefined ? -1 : _fileRows.findIndex((row) => !row.loading && row.path === path);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** The next focusable row from `index`, walking by `step`; -1 at the ends. */
|
|
638
|
+
function fileStep(index: number, step: 1 | -1): number {
|
|
639
|
+
for (let i = index + step; i >= 0 && i < _fileRows.length; i += step) {
|
|
640
|
+
if (!_fileRows[i]!.loading) {
|
|
641
|
+
return i;
|
|
465
642
|
}
|
|
643
|
+
}
|
|
644
|
+
return -1;
|
|
645
|
+
}
|
|
466
646
|
|
|
467
|
-
|
|
468
|
-
|
|
647
|
+
/**
|
|
648
|
+
* Repaint the tree because its window changed.
|
|
649
|
+
*
|
|
650
|
+
* Deferred to a microtask so a scroll arriving mid-commit cannot re-enter the render producing the
|
|
651
|
+
* rows, and skipped during a drag: `registerFileTreeDnD` holds pragmatic-dnd registrations on the
|
|
652
|
+
* rows it can see, and re-rendering under an active drag would drop every one of them.
|
|
653
|
+
*/
|
|
654
|
+
function fileWindowChanged(): void {
|
|
655
|
+
if (_fileList?.isConnected !== true || _fileList.querySelector(".file-tree-item.dragging")) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
queueMicrotask(() => _filesRerender?.());
|
|
659
|
+
}
|
|
469
660
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Adopt the rendered tree: remember it, keep it watching whatever scrolls it, and hand the keyboard
|
|
663
|
+
* the row a jump asked for once that row exists.
|
|
664
|
+
*
|
|
665
|
+
* The first paint of a session draws every row, because nothing can be measured before it exists;
|
|
666
|
+
* the watch's opening measurement is what asks for the second, windowed pass. Idempotent by
|
|
667
|
+
* construction — `watchListWindow` hands back the same watch for the same element and scroller — so
|
|
668
|
+
* calling it after every render costs a comparison.
|
|
669
|
+
*/
|
|
670
|
+
function adoptFileTree(tree: HTMLElement): void {
|
|
671
|
+
_fileList = tree;
|
|
672
|
+
_fileWatch = watchListWindow(_fileWatch, tree, {
|
|
673
|
+
count: () => _fileRows.length,
|
|
674
|
+
onChange: fileWindowChanged,
|
|
675
|
+
rowHeight: fileRowHeight,
|
|
676
|
+
});
|
|
677
|
+
const wanted = _pendingFocusPath;
|
|
678
|
+
if (wanted !== null) {
|
|
679
|
+
// One shot: a focus request that outlived its own repaint is stale, and moving the keyboard
|
|
680
|
+
// Later is worse than never having moved it.
|
|
681
|
+
_pendingFocusPath = null;
|
|
682
|
+
fileRowElement(tree, wanted)?.focus();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Adopt the tree once its rows are in the document.
|
|
688
|
+
*
|
|
689
|
+
* Deferred by a microtask on purpose, exactly as the Outline's `afterTreeRender` is: a `ref` on the
|
|
690
|
+
* tree element commits BEFORE the child part holding the rows, so on a first render the callback
|
|
691
|
+
* would otherwise measure an empty tree.
|
|
692
|
+
*/
|
|
693
|
+
function afterFileTreeRender(tree: HTMLElement): void {
|
|
694
|
+
queueMicrotask(() => adoptFileTree(tree));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** The rendered row for a path, or null when the window does not currently hold it. */
|
|
698
|
+
function fileRowElement(tree: HTMLElement, path: string): HTMLElement | null {
|
|
699
|
+
return tree.querySelector<HTMLElement>(`.file-tree-item[data-path="${CSS.escape(path)}"]`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* The tree's rows, windowed, with the scroll height of the rows it left out reserved either side.
|
|
704
|
+
*
|
|
705
|
+
* The two spacers are `aria-hidden` because a `role="tree"` owns `treeitem`s, and an empty div that
|
|
706
|
+
* exists to be 4 000 pixels tall is not one.
|
|
707
|
+
*/
|
|
708
|
+
function fileTreeBodyTemplate(ctx: {
|
|
709
|
+
openFileFn: (path: string) => void;
|
|
710
|
+
renderLeftPanel: () => void;
|
|
711
|
+
}): TemplateResult {
|
|
712
|
+
// The scroll watch outlives this call and must never repaint through a closure from an earlier
|
|
713
|
+
// One — the Navigator's scheduler is the only thing that knows how to draw this panel.
|
|
714
|
+
_filesRerender = ctx.renderLeftPanel;
|
|
715
|
+
_fileRows = [];
|
|
716
|
+
// Resolved once for the whole tree, not once per row: this function runs on EVERY repaint of the
|
|
717
|
+
// Navigator, and `resolveI18n` canonicalizes every declared tag through `Intl.Locale`.
|
|
718
|
+
const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
|
|
719
|
+
collectFileRows(".", 0, _fileRows, ctx, i18n);
|
|
720
|
+
// Windowed against the PREVIOUS render's element, the only one that exists while this template is
|
|
721
|
+
// Being built. There is none on the first paint, and `listWindow` then answers "all of them".
|
|
722
|
+
const range = listWindow(_fileList, { count: _fileRows.length, rowHeight: fileRowHeight() });
|
|
723
|
+
// The roving tab stop is decided from the MODEL — the first DRAWN row of a windowed tree is
|
|
724
|
+
// Usually not the first row of the tree — and then clamped INTO the window, because a tab stop
|
|
725
|
+
// That is not in the document is not a tab stop: a tree whose selected row has scrolled away
|
|
726
|
+
// Would otherwise have no tabbable row at all, and Tab would skip the whole panel.
|
|
727
|
+
const wanted = Math.max(0, fileIndexOfPath(requireProjectState().selectedPath ?? undefined));
|
|
728
|
+
const tabStop = Math.min(Math.max(wanted, range.start), Math.max(range.start, range.end - 1));
|
|
729
|
+
const slice = _fileRows
|
|
730
|
+
.slice(range.start, range.end)
|
|
731
|
+
.map((row, offset) => ({ row, tabStop: range.start + offset === tabStop }));
|
|
732
|
+
return html`
|
|
733
|
+
<div style="height:${range.padTop}px" aria-hidden="true"></div>
|
|
734
|
+
${repeat(
|
|
735
|
+
slice,
|
|
736
|
+
(entry) => entry.row.key,
|
|
737
|
+
(entry) => fileRowTemplate(entry.row, entry.tabStop, ctx),
|
|
738
|
+
)}
|
|
739
|
+
<div style="height:${range.padBottom}px" aria-hidden="true"></div>
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** One row, drawn. */
|
|
744
|
+
function fileRowTemplate(
|
|
745
|
+
row: FileRow,
|
|
746
|
+
tabStop: boolean,
|
|
747
|
+
ctx: { openFileFn: (path: string) => void; renderLeftPanel: () => void },
|
|
748
|
+
): TemplateResult {
|
|
749
|
+
if (row.loading) {
|
|
750
|
+
return html`<div
|
|
751
|
+
class="file-tree-item"
|
|
752
|
+
style="padding-left:${8 + row.depth * 16}px;color:var(--fg-dim);font-style:italic"
|
|
753
|
+
>
|
|
754
|
+
Loading…
|
|
755
|
+
</div>`;
|
|
756
|
+
}
|
|
757
|
+
const isDir = row.type === "directory";
|
|
758
|
+
return html`
|
|
759
|
+
<div
|
|
760
|
+
class=${classMap({
|
|
761
|
+
"file-tree-item": true,
|
|
762
|
+
selected: requireProjectState().selectedPath === row.path,
|
|
763
|
+
})}
|
|
764
|
+
style="padding-left:${8 + row.depth * 16}px"
|
|
765
|
+
role="treeitem"
|
|
766
|
+
aria-level=${row.depth + 1}
|
|
767
|
+
aria-posinset=${row.posInSet}
|
|
768
|
+
aria-setsize=${row.setSize}
|
|
769
|
+
tabindex=${tabStop ? "0" : "-1"}
|
|
770
|
+
data-path=${row.path}
|
|
771
|
+
data-type=${row.type}
|
|
772
|
+
aria-expanded=${isDir ? String(row.expanded) : nothing}
|
|
773
|
+
@click=${async (e: MouseEvent) => {
|
|
774
|
+
e.stopPropagation();
|
|
775
|
+
if (isDir) {
|
|
776
|
+
await toggleTreeDirectory(row.path);
|
|
777
|
+
ctx.renderLeftPanel();
|
|
778
|
+
} else {
|
|
779
|
+
ctx.openFileFn(row.path);
|
|
495
780
|
}
|
|
496
|
-
|
|
781
|
+
}}
|
|
782
|
+
@contextmenu=${(e: MouseEvent) => {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
e.stopPropagation();
|
|
785
|
+
showFileContextMenu(e, { name: row.name, path: row.path, type: row.type }, ctx);
|
|
786
|
+
}}
|
|
787
|
+
>
|
|
788
|
+
${
|
|
789
|
+
isDir
|
|
790
|
+
? html`<span class="file-tree-toggle">${row.expanded ? "▼" : "▶"}</span>`
|
|
791
|
+
: html`<span class="file-tree-toggle empty"> </span>`
|
|
497
792
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
break;
|
|
793
|
+
<span class="file-tree-icon">${fileTypeIconTpl(row.path, row.type)}</span>
|
|
794
|
+
<span class="file-tree-name">${row.name}</span>
|
|
795
|
+
${
|
|
796
|
+
row.locale === undefined
|
|
797
|
+
? nothing
|
|
798
|
+
: html`<span class="file-tree-locale">${localeLabel(row.locale)}</span>`
|
|
507
799
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
800
|
+
</div>
|
|
801
|
+
`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Expand or collapse one directory, listing it the first time it is opened. */
|
|
805
|
+
async function toggleTreeDirectory(path: string): Promise<void> {
|
|
806
|
+
const state = requireProjectState();
|
|
807
|
+
if (state.expanded.has(path)) {
|
|
808
|
+
state.expanded.delete(path);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
state.expanded.add(path);
|
|
812
|
+
if (!state.dirs.has(path)) {
|
|
813
|
+
await loadDirectory(path);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Tree elements this module has already given a keydown listener. */
|
|
818
|
+
const _keyboardTrees = new WeakSet<HTMLElement>();
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Give the tree its keyboard, once per tree ELEMENT.
|
|
822
|
+
*
|
|
823
|
+
* The panel's `afterRender` calls this after every repaint and lit re-uses the `.file-tree` element
|
|
824
|
+
* across all of them, so the unguarded `addEventListener` it replaces accumulated one listener per
|
|
825
|
+
* render — after ten repaints a single Down keystroke walked ten rows. The WeakSet still lets a
|
|
826
|
+
* remounted panel's new element bind its own.
|
|
827
|
+
*/
|
|
828
|
+
export function setupTreeKeyboard(tree: HTMLElement) {
|
|
829
|
+
if (_keyboardTrees.has(tree)) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
_keyboardTrees.add(tree);
|
|
833
|
+
tree.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
834
|
+
onFileTreeKeydown(e, tree);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* The tree's keyboard model: ↑↓ walk the rows, → expands, ← collapses, Enter opens.
|
|
840
|
+
*
|
|
841
|
+
* ↑ and ↓ step through the MODEL, not through the rendered rows. A DOM-indexed walk stopped dead at
|
|
842
|
+
* the last row of the window — three rows past the bottom of the viewport, with thousands of files
|
|
843
|
+
* still below it — because there was simply no next element to focus. When the step lands outside
|
|
844
|
+
* the window the scroller moves instead, and the focus follows on the repaint ({@link
|
|
845
|
+
* adoptFileTree}).
|
|
846
|
+
*/
|
|
847
|
+
function onFileTreeKeydown(e: KeyboardEvent, tree: HTMLElement): void {
|
|
848
|
+
const focused = tree.querySelector(".file-tree-item:focus") as HTMLElement | null;
|
|
849
|
+
if (!focused) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
let handled = true;
|
|
853
|
+
|
|
854
|
+
switch (e.key) {
|
|
855
|
+
case "ArrowDown":
|
|
856
|
+
case "ArrowUp": {
|
|
857
|
+
const from = fileIndexOfPath(focused.dataset.path);
|
|
858
|
+
focusFileRow(tree, fileStep(from, e.key === "ArrowDown" ? 1 : -1));
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
case "ArrowRight": {
|
|
862
|
+
const path = collapsedDirectoryAt(focused);
|
|
863
|
+
if (path !== null) {
|
|
864
|
+
// The expansion repaints THROUGH the panel. It used to synthesise a click on the focused
|
|
865
|
+
// Row to get a repaint, which ran that row's own toggle a second time and only did the
|
|
866
|
+
// Right thing because the handler's captured `isExpanded` was already stale.
|
|
867
|
+
void toggleTreeDirectory(path).then(() => _filesRerender?.());
|
|
511
868
|
}
|
|
512
|
-
|
|
513
|
-
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
case "ArrowLeft": {
|
|
872
|
+
const path = expandedDirectoryAt(focused);
|
|
873
|
+
if (path !== null) {
|
|
874
|
+
requireProjectState().expanded.delete(path);
|
|
875
|
+
// It used to leave the repaint to "the caller who sets up keyboard", and there was no such
|
|
876
|
+
// Caller: ← changed the state and left the children on screen until something else
|
|
877
|
+
// Happened to redraw the panel.
|
|
878
|
+
_filesRerender?.();
|
|
514
879
|
}
|
|
880
|
+
break;
|
|
515
881
|
}
|
|
516
|
-
|
|
517
|
-
|
|
882
|
+
case "Enter": {
|
|
883
|
+
focused.click();
|
|
884
|
+
break;
|
|
518
885
|
}
|
|
519
|
-
|
|
886
|
+
default: {
|
|
887
|
+
handled = false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (handled) {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** The row's directory path when it is a COLLAPSED directory — what → acts on, and nothing else. */
|
|
896
|
+
function collapsedDirectoryAt(row: HTMLElement): string | null {
|
|
897
|
+
const { path, type } = row.dataset;
|
|
898
|
+
return type === "directory" && path !== undefined && !requireProjectState().expanded.has(path)
|
|
899
|
+
? path
|
|
900
|
+
: null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** The row's directory path when it is an EXPANDED directory — what ← acts on, and nothing else. */
|
|
904
|
+
function expandedDirectoryAt(row: HTMLElement): string | null {
|
|
905
|
+
const { path, type } = row.dataset;
|
|
906
|
+
return type === "directory" && path !== undefined && requireProjectState().expanded.has(path)
|
|
907
|
+
? path
|
|
908
|
+
: null;
|
|
909
|
+
}
|
|
520
910
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
911
|
+
/** Move the keyboard to the model row at `index`, bringing it into the window if it is outside. */
|
|
912
|
+
function focusFileRow(tree: HTMLElement, index: number): void {
|
|
913
|
+
const row = _fileRows[index];
|
|
914
|
+
if (!row) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const el = fileRowElement(tree, row.path);
|
|
918
|
+
if (el) {
|
|
919
|
+
el.focus();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (revealListRow(_fileList, index, fileRowHeight())) {
|
|
923
|
+
_pendingFocusPath = row.path;
|
|
924
|
+
_filesRerender?.();
|
|
525
925
|
}
|
|
526
926
|
}
|
|
527
927
|
|
|
@@ -804,9 +1204,13 @@ async function moveFileEntry(oldPath: string, newPath: string, renderLeftPanel:
|
|
|
804
1204
|
|
|
805
1205
|
reloadRewrittenTabs(report, newPath);
|
|
806
1206
|
renderLeftPanel();
|
|
807
|
-
|
|
1207
|
+
notify.success(`Moved to ${newPath}`);
|
|
808
1208
|
} catch (error) {
|
|
809
|
-
|
|
1209
|
+
notify.error(`Could not move ${oldPath}.`, {
|
|
1210
|
+
detail: errorMessage(error),
|
|
1211
|
+
path: oldPath,
|
|
1212
|
+
source: "Files",
|
|
1213
|
+
});
|
|
810
1214
|
}
|
|
811
1215
|
}
|
|
812
1216
|
|
|
@@ -828,6 +1232,89 @@ function dismissFileContextMenu() {
|
|
|
828
1232
|
}
|
|
829
1233
|
}
|
|
830
1234
|
|
|
1235
|
+
/** One row of the file menu. The divider is the em-dash label; `disabled` rows explain themselves. */
|
|
1236
|
+
interface FileMenuItem {
|
|
1237
|
+
label: string;
|
|
1238
|
+
action?: () => void;
|
|
1239
|
+
danger?: boolean;
|
|
1240
|
+
disabled?: boolean;
|
|
1241
|
+
/** The `requires` sentence, printed under a disabled row. */
|
|
1242
|
+
reason?: string;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* A file row addresses ONE thing, and this is everything it can say about it — keyed by the
|
|
1247
|
+
* ARGUMENT NAME that asks for it.
|
|
1248
|
+
*
|
|
1249
|
+
* `menus: ["context/file"]` names a placement, and a placement nothing renders is the same defect
|
|
1250
|
+
* as a command nothing registers, one layer down: `content.openEntry` declared this menu and the
|
|
1251
|
+
* tree drew a hand-built list beside it, so the row simply never existed. Rendering the placement
|
|
1252
|
+
* is the fix, and it needs the one thing the element menu does not — an argument.
|
|
1253
|
+
* `content.openEntry` wants a `path`, `collection.editInGrid` wants a `name`, and only the row
|
|
1254
|
+
* knows either.
|
|
1255
|
+
*
|
|
1256
|
+
* A fact is stated ONLY when it is true of this row, and that is what decides whether a command
|
|
1257
|
+
* appears at all: `styles/main.css` is an entry of no collection, so it states no `path`, so "Open
|
|
1258
|
+
* Entry Form" is not offered on it. A command whose required arguments this row cannot answer is
|
|
1259
|
+
* skipped — never rendered into a refusal the author cannot act on.
|
|
1260
|
+
*/
|
|
1261
|
+
function fileRowFacts(entry: { path: string; type: string }): Record<string, unknown> {
|
|
1262
|
+
const facts: Record<string, unknown> = {};
|
|
1263
|
+
if (entry.type === "directory") {
|
|
1264
|
+
const collection = collectionDirs().find(
|
|
1265
|
+
({ dir }) => entry.path === dir || entry.path.endsWith(`/${dir}`),
|
|
1266
|
+
);
|
|
1267
|
+
if (collection) {
|
|
1268
|
+
facts.name = collection.name;
|
|
1269
|
+
}
|
|
1270
|
+
} else if (collectionOfPath(entry.path)) {
|
|
1271
|
+
facts.path = entry.path;
|
|
1272
|
+
}
|
|
1273
|
+
return facts;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* The declared `context/file` commands this row can offer.
|
|
1278
|
+
*
|
|
1279
|
+
* Everything a row prints comes off the record — its title, its position (`forPlacement` sorts by
|
|
1280
|
+
* `group`), whether it is enabled and the sentence saying why not. Nothing here names a command, so
|
|
1281
|
+
* a new `context/file` record appears in the tree with no edit to this file.
|
|
1282
|
+
*/
|
|
1283
|
+
function placedFileItems(entry: { path: string; type: string }): FileMenuItem[] {
|
|
1284
|
+
const registry = activeRegistry();
|
|
1285
|
+
if (!registry) {
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
const facts = fileRowFacts(entry);
|
|
1289
|
+
const items: FileMenuItem[] = [];
|
|
1290
|
+
for (const command of registry.forPlacement("context/file")) {
|
|
1291
|
+
const schema = command.args as
|
|
1292
|
+
| { properties?: Record<string, unknown>; required?: readonly string[] }
|
|
1293
|
+
| undefined;
|
|
1294
|
+
if (!(schema?.required ?? []).every((key) => key in facts)) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const args: Record<string, unknown> = {};
|
|
1298
|
+
for (const key of Object.keys(schema?.properties ?? {})) {
|
|
1299
|
+
if (key in facts) {
|
|
1300
|
+
args[key] = facts[key];
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const reason = registry.disabledReason(command.id);
|
|
1304
|
+
items.push(
|
|
1305
|
+
reason === undefined
|
|
1306
|
+
? {
|
|
1307
|
+
action: () => {
|
|
1308
|
+
void registry.run(command.id, args);
|
|
1309
|
+
},
|
|
1310
|
+
label: command.title,
|
|
1311
|
+
}
|
|
1312
|
+
: { disabled: true, label: command.title, reason },
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return items;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
831
1318
|
function showFileContextMenu(
|
|
832
1319
|
e: MouseEvent,
|
|
833
1320
|
entry: { name: string; path: string; type: string },
|
|
@@ -837,8 +1324,7 @@ function showFileContextMenu(
|
|
|
837
1324
|
dismissFileContextMenu();
|
|
838
1325
|
const isDir = entry.type === "directory";
|
|
839
1326
|
|
|
840
|
-
|
|
841
|
-
const items = [];
|
|
1327
|
+
const items: FileMenuItem[] = [];
|
|
842
1328
|
|
|
843
1329
|
if (!isDir) {
|
|
844
1330
|
items.push({ action: () => ctx.openFileFn(entry.path), label: "Open" });
|
|
@@ -854,19 +1340,9 @@ function showFileContextMenu(
|
|
|
854
1340
|
label: "Upload Files\u2026",
|
|
855
1341
|
},
|
|
856
1342
|
);
|
|
857
|
-
// Directories backing a content collection get a bulk-edit affordance.
|
|
858
|
-
const collection = collectionDirs().find(
|
|
859
|
-
({ dir }) => entry.path === dir || entry.path.endsWith(`/${dir}`),
|
|
860
|
-
);
|
|
861
|
-
if (collection) {
|
|
862
|
-
items.push({
|
|
863
|
-
action: () => {
|
|
864
|
-
openCollectionGrid(collection.name);
|
|
865
|
-
},
|
|
866
|
-
label: "Edit Collection in Grid",
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
1343
|
if (entry.path === "pages" || entry.path.endsWith("/pages")) {
|
|
1344
|
+
// The one hand-built row left: no command declares "open the pages grid". `grid-open.ts`'s
|
|
1345
|
+
// `collection.editInGrid` has no pages sibling, so there is nothing here to render yet.
|
|
870
1346
|
items.push({
|
|
871
1347
|
action: () => {
|
|
872
1348
|
openPagesGrid();
|
|
@@ -876,6 +1352,9 @@ function showFileContextMenu(
|
|
|
876
1352
|
}
|
|
877
1353
|
}
|
|
878
1354
|
items.push(
|
|
1355
|
+
// The declared rows sit between what the TREE does to a file (open it, create in it, upload to
|
|
1356
|
+
// It) and what it does to the file's existence (rename, delete).
|
|
1357
|
+
...placedFileItems(entry),
|
|
879
1358
|
{ label: "\u2014" },
|
|
880
1359
|
{
|
|
881
1360
|
action: () => renameFile(entry, ctx.renderLeftPanel),
|
|
@@ -888,8 +1367,8 @@ function showFileContextMenu(
|
|
|
888
1367
|
},
|
|
889
1368
|
);
|
|
890
1369
|
|
|
891
|
-
let x = e.clientX
|
|
892
|
-
|
|
1370
|
+
let x = e.clientX;
|
|
1371
|
+
let y = e.clientY;
|
|
893
1372
|
|
|
894
1373
|
_fileCtxHandle = renderPopover(
|
|
895
1374
|
html`<sp-popover
|
|
@@ -919,11 +1398,20 @@ function showFileContextMenu(
|
|
|
919
1398
|
? html`<sp-menu-divider></sp-menu-divider>`
|
|
920
1399
|
: html`<sp-menu-item
|
|
921
1400
|
style=${item.danger ? "color: var(--danger)" : ""}
|
|
1401
|
+
?disabled=${item.disabled === true}
|
|
1402
|
+
aria-disabled=${item.disabled === true ? "true" : "false"}
|
|
922
1403
|
@click=${() => {
|
|
1404
|
+
if (item.disabled === true) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
923
1407
|
dismissFileContextMenu();
|
|
924
1408
|
void item.action?.();
|
|
925
1409
|
}}
|
|
926
|
-
>${item.label}
|
|
1410
|
+
>${item.label}${
|
|
1411
|
+
// A disabled row says what it needs, the same sentence the palette and the agent
|
|
1412
|
+
// Print — `requires`, off the record, never re-worded here.
|
|
1413
|
+
item.reason ? html`<span slot="description">Needs ${item.reason}</span>` : nothing
|
|
1414
|
+
}</sp-menu-item
|
|
927
1415
|
>`,
|
|
928
1416
|
)}
|
|
929
1417
|
</sp-menu>
|
|
@@ -939,42 +1427,141 @@ function showFileContextMenu(
|
|
|
939
1427
|
|
|
940
1428
|
// ─── File CRUD ────────────────────────────────────────────────────────────────
|
|
941
1429
|
|
|
942
|
-
|
|
943
|
-
|
|
1430
|
+
/** The default body for a path no format claims — a document, since that is what Jx authors. */
|
|
1431
|
+
const BLANK_DOCUMENT = JSON.stringify(
|
|
1432
|
+
{ children: [{ children: [], tagName: "p" }], tagName: "div" },
|
|
1433
|
+
null,
|
|
1434
|
+
2,
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* One creation, named.
|
|
1439
|
+
*
|
|
1440
|
+
* `dir` is required and has no default. That is the whole point of the type: the Library used to
|
|
1441
|
+
* derive its destination from whichever CATEGORY filter happened to be active — and "All" derived
|
|
1442
|
+
* nothing, so a new page landed wherever the writer's fallback pointed. A creation flow that cannot
|
|
1443
|
+
* say where the file is going has no business asking for its name.
|
|
1444
|
+
*/
|
|
1445
|
+
export interface NewFileRequest {
|
|
1446
|
+
/** Destination directory, project-relative. `"."` is the project root. */
|
|
1447
|
+
dir: string;
|
|
1448
|
+
/** Dialog title — "New File", "New Page", "New Post". Defaults to "New File". */
|
|
1449
|
+
title?: string;
|
|
1450
|
+
/** Pre-filled value; its stem is selected so typing replaces the name and keeps the extension. */
|
|
1451
|
+
suggestedName?: string;
|
|
1452
|
+
/**
|
|
1453
|
+
* When set, the field asks for a DISPLAY NAME and this extension is appended to its slug — "My
|
|
1454
|
+
* First Post" becomes `my-first-post.md`. When absent, the field asks for a file name and takes
|
|
1455
|
+
* it verbatim, which is what the Files tree has always done.
|
|
1456
|
+
*/
|
|
1457
|
+
ext?: string;
|
|
1458
|
+
/** Body to write. Defaults to the resolved format's `newFileTemplate`. */
|
|
1459
|
+
content?: string;
|
|
1460
|
+
/** Who is creating, for the Problem's `source` line. Defaults to "Files". */
|
|
1461
|
+
source?: string;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Create one file, from the one flow both the Files tree and the Library use.
|
|
1466
|
+
*
|
|
1467
|
+
* Two behaviours the callers used to disagree about, settled here:
|
|
1468
|
+
*
|
|
1469
|
+
* - **A name that is already taken is refused in the FIELD**, not discovered afterwards. Both
|
|
1470
|
+
* predecessors called `writeFile` straight onto the composed path, so creating `about.md` in a
|
|
1471
|
+
* directory that had one silently replaced it — with no undo, because the file was never open.
|
|
1472
|
+
* The destination is listed once before the prompt so `validate` can say so while it can still be
|
|
1473
|
+
* fixed.
|
|
1474
|
+
* - **A failure is a Problem carrying the path**, not a toast that scrolls away, since the thing the
|
|
1475
|
+
* author must do next is about that path.
|
|
1476
|
+
*
|
|
1477
|
+
* @returns The created path, or `null` when the author cancelled or the write failed.
|
|
1478
|
+
*/
|
|
1479
|
+
export async function createFileIn(request: NewFileRequest): Promise<string | null> {
|
|
1480
|
+
const { dir, ext, source = "Files" } = request;
|
|
1481
|
+
await loadFormats();
|
|
1482
|
+
|
|
1483
|
+
// One listing, before the field opens: the names it must refuse are known while typing.
|
|
1484
|
+
let taken = new Set<string>();
|
|
1485
|
+
try {
|
|
1486
|
+
const listing = await getPlatform().listDirectory(dir);
|
|
1487
|
+
taken = new Set(listing.map((entry) => entry.name));
|
|
1488
|
+
} catch {
|
|
1489
|
+
// A directory that cannot be listed is usually one that does not exist yet — the write below
|
|
1490
|
+
// Is the authority on whether that is a problem, and it reports with the real reason.
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const fileNameFor = (input: string) =>
|
|
1494
|
+
ext === undefined
|
|
1495
|
+
? input.trim()
|
|
1496
|
+
: `${input
|
|
1497
|
+
.trim()
|
|
1498
|
+
.toLowerCase()
|
|
1499
|
+
.replaceAll(/\s+/g, "-")
|
|
1500
|
+
.replaceAll(/[^a-z\d-]/g, "")}${ext}`;
|
|
1501
|
+
|
|
1502
|
+
const entered = await showPromptDialog(request.title ?? "New File", {
|
|
944
1503
|
confirmLabel: "Create",
|
|
945
|
-
message:
|
|
946
|
-
placeholder:
|
|
1504
|
+
message: dir === "." ? "Creating in the project root." : `Creating in ${dir}/`,
|
|
1505
|
+
...(request.suggestedName === undefined ? {} : { placeholder: request.suggestedName }),
|
|
947
1506
|
select: "stem",
|
|
948
|
-
validate: (
|
|
949
|
-
|
|
1507
|
+
validate: (value) => {
|
|
1508
|
+
if (!value.trim()) {
|
|
1509
|
+
return ext === undefined ? "Enter a file name." : "Enter a name.";
|
|
1510
|
+
}
|
|
1511
|
+
const candidate = fileNameFor(value);
|
|
1512
|
+
if (!candidate || candidate === ext) {
|
|
1513
|
+
return "Enter at least one letter or number.";
|
|
1514
|
+
}
|
|
1515
|
+
return taken.has(candidate) ? `${candidate} already exists in ${dir}/.` : "";
|
|
1516
|
+
},
|
|
1517
|
+
value: request.suggestedName ?? "untitled.json",
|
|
950
1518
|
});
|
|
951
|
-
if (!
|
|
952
|
-
return;
|
|
1519
|
+
if (!entered) {
|
|
1520
|
+
return null;
|
|
953
1521
|
}
|
|
954
|
-
|
|
1522
|
+
|
|
1523
|
+
const fileName = fileNameFor(entered);
|
|
1524
|
+
const path = dir === "." ? fileName : `${dir}/${fileName}`;
|
|
955
1525
|
markLocalMutation(path);
|
|
956
|
-
|
|
957
|
-
const format = formatForPath(name);
|
|
1526
|
+
const format = formatForPath(fileName);
|
|
958
1527
|
const content =
|
|
959
|
-
format?.studio?.newFileTemplate ??
|
|
960
|
-
(format
|
|
961
|
-
? ""
|
|
962
|
-
: JSON.stringify({ children: [{ children: [], tagName: "p" }], tagName: "div" }, null, 2));
|
|
1528
|
+
request.content ?? format?.studio?.newFileTemplate ?? (format ? "" : BLANK_DOCUMENT);
|
|
963
1529
|
try {
|
|
964
|
-
|
|
965
|
-
await
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
statusMessage(`Created ${path}`);
|
|
1530
|
+
await getPlatform().writeFile(path, content);
|
|
1531
|
+
await loadDirectory(dir);
|
|
1532
|
+
notify.success(`Created ${path}`);
|
|
1533
|
+
return path;
|
|
969
1534
|
} catch (error) {
|
|
970
|
-
|
|
1535
|
+
notify.error(`Could not create ${path}.`, {
|
|
1536
|
+
detail: errorMessage(error),
|
|
1537
|
+
path,
|
|
1538
|
+
source,
|
|
1539
|
+
});
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function createNewFile(dirPath: string, renderLeftPanel: () => void) {
|
|
1545
|
+
const created = await createFileIn({ dir: dirPath, suggestedName: "untitled.json" });
|
|
1546
|
+
if (created !== null) {
|
|
1547
|
+
renderLeftPanel();
|
|
971
1548
|
}
|
|
972
1549
|
}
|
|
973
1550
|
|
|
974
|
-
/**
|
|
975
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* The rename dialog, carrying what moves with the file.
|
|
1553
|
+
*
|
|
1554
|
+
* `renamePromptMessage` is awaited before the field opens, so the count is on screen when the name
|
|
1555
|
+
* is typed rather than after it is confirmed — the refactor pass is about to rewrite every one of
|
|
1556
|
+
* those references, and silently doing that much work was the previous behaviour.
|
|
1557
|
+
*
|
|
1558
|
+
* @param {string} currentName @param {string} path @returns {Promise<string | null>}
|
|
1559
|
+
*/
|
|
1560
|
+
async function showRenameFileDialog(currentName: string, path: string): Promise<string | null> {
|
|
1561
|
+
const message = await renamePromptMessage(path);
|
|
976
1562
|
return showPromptDialog("Rename", {
|
|
977
1563
|
confirmLabel: "Rename",
|
|
1564
|
+
...(message === undefined ? {} : { message }),
|
|
978
1565
|
select: "stem",
|
|
979
1566
|
validate: (v) => (v.trim() ? "" : "Enter a file name."),
|
|
980
1567
|
value: currentName,
|
|
@@ -1007,7 +1594,7 @@ async function renameFile(
|
|
|
1007
1594
|
entry: { name: string; path: string; type: string },
|
|
1008
1595
|
renderLeftPanel: () => void,
|
|
1009
1596
|
) {
|
|
1010
|
-
const newName = await showRenameFileDialog(entry.name);
|
|
1597
|
+
const newName = await showRenameFileDialog(entry.name, entry.path);
|
|
1011
1598
|
if (!newName || newName === entry.name) {
|
|
1012
1599
|
return;
|
|
1013
1600
|
}
|
|
@@ -1020,6 +1607,9 @@ async function renameFile(
|
|
|
1020
1607
|
try {
|
|
1021
1608
|
const platform = getPlatform();
|
|
1022
1609
|
const report = await platform.renameFile(entry.path, newPath);
|
|
1610
|
+
// The refactor pass just rewrote references project-wide, and `markLocalMutation` suppresses
|
|
1611
|
+
// The watcher echo that would otherwise say so — hence the explicit drop.
|
|
1612
|
+
invalidateUsages();
|
|
1023
1613
|
await loadDirectory(parentDirPath);
|
|
1024
1614
|
if (requireProjectState().selectedPath === entry.path) {
|
|
1025
1615
|
requireProjectState().selectedPath = newPath;
|
|
@@ -1029,9 +1619,13 @@ async function renameFile(
|
|
|
1029
1619
|
}
|
|
1030
1620
|
reloadRewrittenTabs(report, newPath);
|
|
1031
1621
|
renderLeftPanel();
|
|
1032
|
-
|
|
1622
|
+
notify.success(renameStatus(newName, report));
|
|
1033
1623
|
} catch (error) {
|
|
1034
|
-
|
|
1624
|
+
notify.error(`Could not rename ${entry.name}.`, {
|
|
1625
|
+
detail: errorMessage(error),
|
|
1626
|
+
path: entry.path,
|
|
1627
|
+
source: "Files",
|
|
1628
|
+
});
|
|
1035
1629
|
}
|
|
1036
1630
|
}
|
|
1037
1631
|
|
|
@@ -1039,10 +1633,7 @@ async function deleteFile(
|
|
|
1039
1633
|
entry: { name: string; path: string; type: string },
|
|
1040
1634
|
renderLeftPanel: () => void,
|
|
1041
1635
|
) {
|
|
1042
|
-
const confirmed = await
|
|
1043
|
-
confirmLabel: "Delete",
|
|
1044
|
-
destructive: true,
|
|
1045
|
-
});
|
|
1636
|
+
const confirmed = await confirmFileDelete(entry);
|
|
1046
1637
|
if (!confirmed) {
|
|
1047
1638
|
return;
|
|
1048
1639
|
}
|
|
@@ -1050,6 +1641,7 @@ async function deleteFile(
|
|
|
1050
1641
|
const platform = getPlatform();
|
|
1051
1642
|
markLocalMutation(entry.path);
|
|
1052
1643
|
await platform.deleteFile(entry.path);
|
|
1644
|
+
invalidateUsages();
|
|
1053
1645
|
const delPath = entry.path.replaceAll("\\", "/");
|
|
1054
1646
|
const parentDirPath = delPath.includes("/") ? delPath.slice(0, delPath.lastIndexOf("/")) : ".";
|
|
1055
1647
|
await loadDirectory(parentDirPath);
|
|
@@ -1057,96 +1649,77 @@ async function deleteFile(
|
|
|
1057
1649
|
requireProjectState().selectedPath = null;
|
|
1058
1650
|
}
|
|
1059
1651
|
renderLeftPanel();
|
|
1060
|
-
|
|
1652
|
+
notify.success(`Deleted ${entry.name}`);
|
|
1061
1653
|
} catch (error) {
|
|
1062
|
-
|
|
1654
|
+
notify.error(`Could not delete ${entry.name}.`, {
|
|
1655
|
+
detail: errorMessage(error),
|
|
1656
|
+
path: entry.path,
|
|
1657
|
+
source: "Files",
|
|
1658
|
+
});
|
|
1063
1659
|
}
|
|
1064
1660
|
}
|
|
1065
1661
|
|
|
1066
|
-
// ─── Open file from tree ──────────────────────────────────────────────────────
|
|
1067
|
-
|
|
1068
1662
|
/**
|
|
1069
|
-
*
|
|
1663
|
+
* What an open can say beyond the path. Every field defaults to today's answer.
|
|
1070
1664
|
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* }} ctx
|
|
1077
|
-
* @param {string} path
|
|
1665
|
+
* Deliberately one inline-typed `opts` object rather than a `paneId` parameter:
|
|
1666
|
+
* `scripts/check-pane-singletons.ts` rule 4 charges any function whose parameters NAME a pane for
|
|
1667
|
+
* reading the focus, one hop in — and {@link openFileInTab} legitimately falls back to the focused
|
|
1668
|
+
* pane when nobody names one. {@link openFileInPane} is the named sibling for readers who want the
|
|
1669
|
+
* pane in the signature; it is pane-scoped and reads no focus of its own.
|
|
1078
1670
|
*/
|
|
1079
|
-
export
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
) {
|
|
1088
|
-
const platform = getPlatform();
|
|
1089
|
-
await loadFormats();
|
|
1090
|
-
// Auto-save current dirty document
|
|
1091
|
-
if (ctx.S.dirty && ctx.S.documentPath) {
|
|
1092
|
-
try {
|
|
1093
|
-
const tabLike = {
|
|
1094
|
-
doc: {
|
|
1095
|
-
content: ctx.S.content,
|
|
1096
|
-
document: ctx.S.document,
|
|
1097
|
-
mode: ctx.S.mode,
|
|
1098
|
-
sourceFormat: formatForPath(ctx.S.documentPath)?.name ?? null,
|
|
1099
|
-
},
|
|
1100
|
-
} as unknown as Tab;
|
|
1101
|
-
const output = await serializeDocument(tabLike);
|
|
1102
|
-
await platform.writeFile(ctx.S.documentPath, output);
|
|
1103
|
-
} catch (error) {
|
|
1104
|
-
statusMessage(`Save error: ${errorMessage(error)}`);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1671
|
+
export interface OpenFileOpts {
|
|
1672
|
+
/** Which pane. Defaults to the focused one. */
|
|
1673
|
+
paneId?: string;
|
|
1674
|
+
/** Open as a disposable preview tab (§4.3) — browsing rather than committing. */
|
|
1675
|
+
preview?: boolean;
|
|
1676
|
+
/** False leaves the keyboard where it is. Defaults to true. */
|
|
1677
|
+
focus?: boolean;
|
|
1678
|
+
}
|
|
1107
1679
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1680
|
+
/**
|
|
1681
|
+
* Bring an ALREADY-OPEN tab to where the caller asked for it.
|
|
1682
|
+
*
|
|
1683
|
+
* Three cases, and the third is the one a following pane depends on:
|
|
1684
|
+
*
|
|
1685
|
+
* | the tab is… | behaviour |
|
|
1686
|
+
* | ------------------------------------------------ | ---------------------------------------------- |
|
|
1687
|
+
* | in the requested pane (or no pane was requested) | activate it there, honouring `focus` |
|
|
1688
|
+
* | elsewhere, and **not** its pane's active tab | move it — one tab is one document in one strip |
|
|
1689
|
+
* | elsewhere, and **is** its pane's active tab | **nothing.** You are already looking at it |
|
|
1690
|
+
*
|
|
1691
|
+
* The third exists because moving it would oscillate: a derivation that re-resolves to the document
|
|
1692
|
+
* the author is editing would yank it out of their pane and into the assistant one, and the follow
|
|
1693
|
+
* would then re-resolve against whatever landed in its place.
|
|
1694
|
+
*/
|
|
1695
|
+
function revealOpenTab(tabId: string, opts: OpenFileOpts): void {
|
|
1696
|
+
const wanted = opts.paneId;
|
|
1697
|
+
const holder = paneOfTab(tabId);
|
|
1698
|
+
if (wanted !== undefined && holder && holder.id !== wanted) {
|
|
1699
|
+
if (holder.activeTabId === tabId) {
|
|
1112
1700
|
return;
|
|
1113
1701
|
}
|
|
1114
|
-
|
|
1115
|
-
if (formatForPath(path)) {
|
|
1116
|
-
ctx.loadMarkdown(content, null);
|
|
1117
|
-
ctx.S.documentPath = path;
|
|
1118
|
-
ctx.S.dirty = false;
|
|
1119
|
-
ctx.commit(ctx.S);
|
|
1120
|
-
} else if (path.endsWith(".json")) {
|
|
1121
|
-
const doc = JSON.parse(content) as JxMutableNode;
|
|
1122
|
-
const newS = createState(doc);
|
|
1123
|
-
newS.documentPath = path;
|
|
1124
|
-
newS.dirty = false;
|
|
1125
|
-
ctx.commit(newS);
|
|
1126
|
-
} else {
|
|
1127
|
-
throw noFormatError(path);
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Update tree selection
|
|
1131
|
-
requireProjectState().selectedPath = path;
|
|
1132
|
-
|
|
1133
|
-
ctx.render();
|
|
1134
|
-
statusMessage(`Opened ${path}`);
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
statusMessage(`Error: ${errorMessage(error)}`);
|
|
1702
|
+
moveTabToPane(tabId, wanted);
|
|
1137
1703
|
}
|
|
1704
|
+
activateTab(tabId, { focus: opts.focus !== false });
|
|
1138
1705
|
}
|
|
1139
1706
|
|
|
1140
1707
|
/**
|
|
1141
|
-
* Open a file from the tree into a tab. Activates existing tab if already open.
|
|
1708
|
+
* Open a file from the tree into a tab. Activates the existing tab if it is already open.
|
|
1142
1709
|
*
|
|
1143
1710
|
* @param {string} path
|
|
1711
|
+
* @param {OpenFileOpts} [opts]
|
|
1144
1712
|
*/
|
|
1145
|
-
export async function openFileInTab(path: string) {
|
|
1713
|
+
export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
1714
|
+
const follows = opts.focus !== false;
|
|
1146
1715
|
for (const [id, tab] of workspace.tabs.entries()) {
|
|
1147
1716
|
if (tab.documentPath === path) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1717
|
+
revealOpenTab(id, opts);
|
|
1718
|
+
// The tree's cursor answers "where is the author", so a side-open that deliberately left the
|
|
1719
|
+
// Keyboard behind must not move it.
|
|
1720
|
+
if (follows) {
|
|
1721
|
+
requireProjectState().selectedPath = path;
|
|
1722
|
+
}
|
|
1150
1723
|
return;
|
|
1151
1724
|
}
|
|
1152
1725
|
}
|
|
@@ -1161,9 +1734,12 @@ export async function openFileInTab(path: string) {
|
|
|
1161
1734
|
path,
|
|
1162
1735
|
root: requireProjectState().projectRoot,
|
|
1163
1736
|
});
|
|
1164
|
-
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
1165
1737
|
} catch (error) {
|
|
1166
|
-
|
|
1738
|
+
notify.error(`Could not open ${path}.`, {
|
|
1739
|
+
detail: errorMessage(error),
|
|
1740
|
+
path,
|
|
1741
|
+
source: "Open File",
|
|
1742
|
+
});
|
|
1167
1743
|
}
|
|
1168
1744
|
return;
|
|
1169
1745
|
}
|
|
@@ -1196,20 +1772,41 @@ export async function openFileInTab(path: string) {
|
|
|
1196
1772
|
document,
|
|
1197
1773
|
...(frontmatter != null && { frontmatter }),
|
|
1198
1774
|
sourceFormat: format?.name ?? null,
|
|
1775
|
+
...(opts.paneId !== undefined && { paneId: opts.paneId }),
|
|
1776
|
+
...(opts.preview === true && { preview: true }),
|
|
1777
|
+
...(opts.focus === false && { focus: false }),
|
|
1199
1778
|
});
|
|
1200
|
-
|
|
1779
|
+
if (follows) {
|
|
1780
|
+
requireProjectState().selectedPath = path;
|
|
1781
|
+
}
|
|
1201
1782
|
trackRecentFile({
|
|
1202
1783
|
name: path.split("/").pop() || path,
|
|
1203
1784
|
path,
|
|
1204
1785
|
root: requireProjectState().projectRoot,
|
|
1205
1786
|
});
|
|
1206
|
-
|
|
1207
|
-
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
1208
1787
|
} catch (error) {
|
|
1209
|
-
|
|
1788
|
+
notify.error(`Could not open ${path}.`, {
|
|
1789
|
+
detail: errorMessage(error),
|
|
1790
|
+
path,
|
|
1791
|
+
source: "Open File",
|
|
1792
|
+
});
|
|
1210
1793
|
}
|
|
1211
1794
|
}
|
|
1212
1795
|
|
|
1796
|
+
/**
|
|
1797
|
+
* Open a file into a NAMED pane, browsing rather than committing, leaving the keyboard behind.
|
|
1798
|
+
*
|
|
1799
|
+
* The same body as {@link openFileInTab} with the three options a side-open always wants, given a
|
|
1800
|
+
* signature that says which pane in the first parameter. It is what "open it beside this" means
|
|
1801
|
+
* everywhere it is asked for — drilling into a component, following a layout, `pane.compareWith`.
|
|
1802
|
+
*
|
|
1803
|
+
* @param {string} paneId
|
|
1804
|
+
* @param {string} path
|
|
1805
|
+
*/
|
|
1806
|
+
export async function openFileInPane(paneId: string, path: string): Promise<void> {
|
|
1807
|
+
await openFileInTab(path, { focus: false, paneId, preview: true });
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1213
1810
|
/**
|
|
1214
1811
|
* Reload an already-open tab from disk without changing the active tab. Used to refresh after AI
|
|
1215
1812
|
* assistant writes to a file.
|
|
@@ -1249,8 +1846,41 @@ export async function reloadFileInTab(path: string) {
|
|
|
1249
1846
|
tab.doc.document = JSON.parse(content) as JxMutableNode;
|
|
1250
1847
|
}
|
|
1251
1848
|
tab.doc.dirty = false;
|
|
1252
|
-
} catch {
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
// A file that changed on disk and cannot be re-read leaves the open tab showing the OLD
|
|
1851
|
+
// Document with no indication that it is stale — the most expensive silence in this file.
|
|
1852
|
+
notify.error(`Could not reload ${path} after it changed on disk.`, {
|
|
1853
|
+
detail: errorMessage(error),
|
|
1854
|
+
key: `reload:${path}`,
|
|
1855
|
+
path,
|
|
1856
|
+
source: "Files",
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1253
1859
|
return;
|
|
1254
1860
|
}
|
|
1255
1861
|
}
|
|
1256
1862
|
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Contribute the Files panel.
|
|
1866
|
+
*
|
|
1867
|
+
* `level: "project"` because it WRITES project files — create, rename, delete, move. It reads the
|
|
1868
|
+
* focused document only to highlight a row, and principle 3 files a surface by what it writes.
|
|
1869
|
+
*/
|
|
1870
|
+
export function registerFilesPanel(): void {
|
|
1871
|
+
registerPanel({
|
|
1872
|
+
id: "files",
|
|
1873
|
+
title: "Files",
|
|
1874
|
+
level: "project",
|
|
1875
|
+
dock: "navigator",
|
|
1876
|
+
icon: "sp-icon-folder",
|
|
1877
|
+
render: (ctx) => ctx.deps.renderFilesTemplate(),
|
|
1878
|
+
afterRender: (ctx, host) => {
|
|
1879
|
+
const tree = host.querySelector(".file-tree") as HTMLElement | null;
|
|
1880
|
+
if (tree) {
|
|
1881
|
+
ctx.deps.setupTreeKeyboard(tree);
|
|
1882
|
+
}
|
|
1883
|
+
ctx.deps.registerFileTreeDnD({ renderLeftPanel: ctx.rerender });
|
|
1884
|
+
},
|
|
1885
|
+
});
|
|
1886
|
+
}
|