@jxsuite/studio 5.0.0 → 5.2.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/dist/chunks/{cf-account-picker-fyf63pmy.js → cf-account-picker-h4dab9bq.js} +10 -10
- package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
- package/dist/chunks/components-5y78e05d.js +20 -0
- package/dist/chunks/content-source-sgg1sv6n.js +46 -0
- package/dist/chunks/{convert-to-repeater-d0cgnb6d.js → convert-to-repeater-mf1cxgfn.js} +25 -25
- package/dist/chunks/entry-commands-w9qsxa6q.js +42 -0
- package/dist/chunks/{extension-sections-fy7q4fe9.js → extension-sections-c6zwzbre.js} +27 -27
- package/dist/chunks/file-ops-vrpq1jjk.js +47 -0
- package/dist/chunks/{files-ef8fgs3w.js → files-msaf95y6.js} +33 -32
- package/dist/chunks/{format-host-3c33nwr8.js → format-host-f64fnesp.js} +2 -2
- package/dist/chunks/{fs-events-13tj6sex.js → fs-events-3y6y0nrm.js} +6 -6
- package/dist/chunks/git-panel-bv5e22j0.js +50 -0
- package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
- package/dist/chunks/grid-view-paj67nr5.js +15322 -0
- package/dist/chunks/{iframe-host-jgesd924.js → iframe-host-dh5np8vk.js} +14 -14
- package/dist/chunks/{layers-1nq5evsh.js → layers-1vcfdbet.js} +7 -6
- package/dist/chunks/layers-panel-fksbdg4q.js +40 -0
- package/dist/chunks/{media-picker-2qbetq73.js → media-picker-p49nhqhs.js} +14 -14
- package/dist/chunks/menu-qht48vnc.js +15 -0
- package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
- package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
- package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
- package/dist/chunks/publish-panel-vz23wwmg.js +27 -0
- package/dist/chunks/quick-search-e9rp74m8.js +46 -0
- package/dist/chunks/{references-n553f0e1.js → references-h69xp9zz.js} +4 -4
- package/dist/chunks/right-panel-j97t2yvy.js +45 -0
- package/dist/chunks/{studio-18avnhjp.js → studio-01jeck2y.js} +36 -30
- package/dist/chunks/{studio-9xxz1q69.js → studio-14d9tq4d.js} +9 -9
- package/dist/chunks/{studio-xsykpvsh.js → studio-15f3kxqe.js} +19 -19
- package/dist/chunks/studio-1tbjfqv4.js +146 -0
- package/dist/chunks/{studio-79y5sz51.js → studio-28vs5k2y.js} +2 -2
- package/dist/chunks/{studio-3hxkd92t.js → studio-2mdbbc76.js} +10 -10
- package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
- package/dist/chunks/{studio-8jn5yhyn.js → studio-4c041r8y.js} +3 -3
- package/dist/chunks/{studio-qahxvnan.js → studio-4r14yzyy.js} +4 -4
- package/dist/chunks/{studio-arfpe389.js → studio-4xmjcrg3.js} +6 -6
- package/dist/chunks/{studio-5523bpzt.js → studio-5nz00bhh.js} +3 -3
- package/dist/chunks/{studio-x3rty6tr.js → studio-5t7ena5z.js} +8 -8
- package/dist/chunks/{studio-5rt7sqa0.js → studio-66arbpc2.js} +13 -19
- package/dist/chunks/{studio-fabxk6r9.js → studio-68fydsrj.js} +15 -15
- package/dist/chunks/{studio-tb3ev01j.js → studio-9j6sfeba.js} +6 -6
- package/dist/chunks/{studio-akbww0j5.js → studio-a1pswps3.js} +38 -38
- package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
- package/dist/chunks/{studio-4m0haseg.js → studio-c07s5ckd.js} +4 -4
- package/dist/chunks/{studio-57mf66w2.js → studio-ctm7bhx5.js} +14 -14
- package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
- package/dist/chunks/{studio-vz68yn9e.js → studio-en82c639.js} +6 -6
- package/dist/chunks/{studio-rkstz940.js → studio-etvx9s2g.js} +21 -12
- package/dist/chunks/{studio-az3zd49t.js → studio-ez11dmdh.js} +127 -37
- package/dist/chunks/{studio-sgdc5pfc.js → studio-hdc2jt3z.js} +4 -4
- package/dist/chunks/{studio-s6z853aq.js → studio-hhskq2mr.js} +24 -23
- package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
- package/dist/chunks/{studio-25hkdrkw.js → studio-mpzwze9x.js} +12 -12
- package/dist/chunks/{studio-8qtrpzr3.js → studio-ncn9e189.js} +7 -7
- package/dist/chunks/{studio-81390n6f.js → studio-nq5yf5mp.js} +1 -1
- package/dist/chunks/{studio-5jy81t3s.js → studio-p6pmwkvh.js} +229 -24
- package/dist/chunks/{studio-3gxv60tn.js → studio-qa7fzxan.js} +4 -4
- package/dist/chunks/{studio-7xptrg0j.js → studio-sar8g54p.js} +8 -8
- package/dist/chunks/studio-t171hxhe.js +23 -0
- package/dist/chunks/{studio-em9nw8eh.js → studio-t3adrr56.js} +20 -17
- package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
- package/dist/chunks/{studio-pm6hgy7r.js → studio-vt0tj32j.js} +29 -42
- package/dist/chunks/{studio-d1ja2x9r.js → studio-wjpgxywa.js} +4 -4
- package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
- package/dist/chunks/{studio-rpyah2ew.js → studio-x10fp4tp.js} +64 -24
- package/dist/chunks/{studio-26d7myd1.js → studio-x72j58vw.js} +4 -4
- package/dist/chunks/{studio-cpvb2ed1.js → studio-y4f9fdge.js} +8 -8
- package/dist/chunks/{studio-erdatzvp.js → studio-ympe0n7a.js} +35 -6
- package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
- package/dist/chunks/studio-zjjde71a.js +148 -0
- package/dist/chunks/{workspace-wrew8zk4.js → workspace-bnw54cha.js} +18 -10
- package/dist/iframe-entry.js +102 -11
- package/dist/studio.css +1 -2
- package/dist/studio.js +1113 -16062
- package/package.json +11 -11
- package/src/canvas/canvas-utils.ts +29 -2
- package/src/commands/command-args.ts +37 -2
- package/src/commands/registry.ts +18 -8
- package/src/commands/run-reported.ts +120 -0
- package/src/editor/shortcuts.ts +11 -23
- package/src/files/components.ts +39 -0
- package/src/files/files.ts +41 -17
- package/src/grid/grid-lazy.ts +59 -0
- package/src/grid/grid-open.ts +20 -3
- package/src/grid/grid-panel.ts +43 -9
- package/src/i18n/i18n-commands.ts +10 -0
- package/src/media/media-open.ts +14 -2
- package/src/packages/pull-package-sync.ts +5 -1
- package/src/panels/ai-panel.ts +5 -2
- package/src/panels/block-action-bar.ts +7 -2
- package/src/panels/i18n-panel.ts +8 -15
- package/src/panels/jump-bar.ts +3 -2
- package/src/panels/pane-context.ts +5 -4
- package/src/panels/pane-grid.ts +100 -10
- package/src/panels/problems-panel.ts +6 -2
- package/src/panels/quick-search.ts +6 -12
- package/src/panels/settings-menu.ts +5 -15
- package/src/panels/tab-drop.ts +220 -0
- package/src/panels/tab-strip.ts +238 -33
- package/src/services/ai-command-tools.ts +78 -8
- package/src/services/ai-models.ts +13 -0
- package/src/services/ai-project-tools.ts +8 -2
- package/src/services/ai-system-prompt.ts +4 -8
- package/src/services/ai-tools.ts +8 -69
- package/src/services/document-assistant.ts +9 -6
- package/src/studio.ts +24 -8
- package/src/surfaces/commandbar.json +35 -24
- package/src/surfaces/commandbar.ts +6 -5
- package/src/surfaces/pane-grid.json +35 -3
- package/src/surfaces/pane-grid.ts +26 -4
- package/src/surfaces/statusbar.ts +2 -1
- package/src/surfaces/tab-strip.json +16 -39
- package/src/surfaces/tab-strip.ts +76 -12
- package/src/tabs/project-config.ts +45 -11
- package/src/ui/ai-credentials-form.ts +12 -1
- package/src/ui/layers.ts +4 -1
- package/src/workspace/pane-derive.ts +7 -5
- package/src/workspace/session.ts +3 -3
- package/src/workspace/workspace.ts +220 -43
- package/styles/shell-frame.css +8 -0
- package/styles/shell-frame.json +4 -0
- package/dist/chunks/components-2w1f35qs.js +0 -18
- package/dist/chunks/content-source-3wqeas45.js +0 -46
- package/dist/chunks/entry-commands-d6g50j7s.js +0 -42
- package/dist/chunks/file-ops-3btj6st0.js +0 -47
- package/dist/chunks/git-panel-4wp2rn9b.js +0 -49
- package/dist/chunks/layers-panel-ar2gh90g.js +0 -40
- package/dist/chunks/menu-rf7n72mf.js +0 -14
- package/dist/chunks/publish-panel-98f7sr3g.js +0 -27
- package/dist/chunks/quick-search-srzz83k2.js +0 -45
- package/dist/chunks/right-panel-rrgx2dn3.js +0 -45
- package/dist/chunks/studio-d055c37e.js +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -59,23 +59,23 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
|
|
62
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.
|
|
63
|
-
"@jxsuite/ai": "^0.37.
|
|
62
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.2",
|
|
63
|
+
"@jxsuite/ai": "^0.37.2",
|
|
64
64
|
"@jxsuite/collab": "^0.9.0",
|
|
65
|
-
"@jxsuite/create": "^1.3.
|
|
66
|
-
"@jxsuite/formulas": "^0.0.
|
|
65
|
+
"@jxsuite/create": "^1.3.11",
|
|
66
|
+
"@jxsuite/formulas": "^0.0.18",
|
|
67
67
|
"@jxsuite/markup": "^0.4.10",
|
|
68
|
-
"@jxsuite/protocol": "^2.3.
|
|
69
|
-
"@jxsuite/runtime": "^4.0.
|
|
68
|
+
"@jxsuite/protocol": "^2.3.1",
|
|
69
|
+
"@jxsuite/runtime": "^4.0.1",
|
|
70
70
|
"@jxsuite/schema": "^2.2.0",
|
|
71
|
-
"@jxsuite/site": "^2.0.
|
|
72
|
-
"@jxsuite/ui": "^0.
|
|
73
|
-
"@vue/reactivity": "3.5.
|
|
71
|
+
"@jxsuite/site": "^2.0.1",
|
|
72
|
+
"@jxsuite/ui": "^0.2.0",
|
|
73
|
+
"@vue/reactivity": "3.5.43",
|
|
74
74
|
"ajv": "^8.20.0",
|
|
75
75
|
"ajv-formats": "^3.0.1",
|
|
76
76
|
"lit-html": "^3.3.3",
|
|
77
77
|
"monaco-editor": "^0.56.0",
|
|
78
|
-
"tabulator-tables": "^6.5.
|
|
78
|
+
"tabulator-tables": "^6.5.3",
|
|
79
79
|
"yaml": "^2.9.1"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
@@ -1024,8 +1024,35 @@ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
|
|
|
1024
1024
|
group: "3_canvas",
|
|
1025
1025
|
requires: "an open document",
|
|
1026
1026
|
when: documentOpen,
|
|
1027
|
-
/*
|
|
1028
|
-
|
|
1027
|
+
/*
|
|
1028
|
+
* Projected, and it was deleted once under §12.4's first rule (chrome). The rule is about a
|
|
1029
|
+
* verb whose WHOLE effect is what the person is looking at, and this one's is not: the tree
|
|
1030
|
+
* tools are gated on `editor.kind`, which is derived from the mode (`editorKindForMode`), so
|
|
1031
|
+
* a document the person left in Code — or the model itself moved — refuses every tree write
|
|
1032
|
+
* with "requires an open document whose element tree the canvas is editing", and without this
|
|
1033
|
+
* verb the model had no way back to a state it can read. `run` acts on the active tab and
|
|
1034
|
+
* the model reads the result: `after.editor.kind` is the same fact the gate reads.
|
|
1035
|
+
*/
|
|
1036
|
+
aiTool: {
|
|
1037
|
+
description:
|
|
1038
|
+
"Switch the active document's editor mode. edit and design put its element tree on " +
|
|
1039
|
+
"the canvas, where the document tools work; preview renders it as a visitor sees it; " +
|
|
1040
|
+
"source shows the JSON; stylebook, grid and git-diff open other editors. Use it to " +
|
|
1041
|
+
"return a document to edit or design when the tree tools are refused.",
|
|
1042
|
+
name: "set_canvas_mode",
|
|
1043
|
+
/* `after.editor.kind` rather than the tab's own field: it is the gate's read (`treeEditable`
|
|
1044
|
+
in `document-assistant.ts` is `editor.kind === "canvas"`), so the sentence and the gate
|
|
1045
|
+
cannot disagree. The mode named is the one requested — `run` resolved, so it is the mode
|
|
1046
|
+
the tab is in. */
|
|
1047
|
+
report: ({ after, args }) => {
|
|
1048
|
+
const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
|
|
1049
|
+
const reach =
|
|
1050
|
+
after.editor.kind === "canvas"
|
|
1051
|
+
? "the document tools address its element tree"
|
|
1052
|
+
: "the tree-editing tools are unavailable until it returns to edit or design";
|
|
1053
|
+
return `The active document is in ${mode} mode; ${reach}.`;
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1029
1056
|
run: (_commandCtx, args) => {
|
|
1030
1057
|
const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
|
|
1031
1058
|
const tab = requireTab("canvas.setMode");
|
|
@@ -72,6 +72,23 @@ export function numberArg(commandId: string, args: CommandArgValues, key: string
|
|
|
72
72
|
return value;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A required integer argument. `1.5` is a refusal, not a value to round: a caller naming an index
|
|
77
|
+
* or a count with a fraction in it has said something the app cannot mean.
|
|
78
|
+
*
|
|
79
|
+
* Its own reader rather than `numberArg` plus a check in `run`, because `coerceArgs` routes a bare
|
|
80
|
+
* `type: "integer"` here — without it the row read as a plain number and `1.5` passed the schema
|
|
81
|
+
* pass to fail, or not, in whatever `run` did with it. `typedArg` already held a type LIST to
|
|
82
|
+
* integrality; this is the same rule for the bare form.
|
|
83
|
+
*/
|
|
84
|
+
export function integerArg(commandId: string, args: CommandArgValues, key: string): number {
|
|
85
|
+
const value = args[key];
|
|
86
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
87
|
+
throw refuse(commandId, key, `expected an integer, got ${describe(value)}`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
/**
|
|
76
93
|
* A required number argument inside a closed interval.
|
|
77
94
|
*
|
|
@@ -330,6 +347,7 @@ export type CoercionRow =
|
|
|
330
347
|
| "boolean"
|
|
331
348
|
| "bounded-number"
|
|
332
349
|
| "number"
|
|
350
|
+
| "integer"
|
|
333
351
|
| "string"
|
|
334
352
|
| "path"
|
|
335
353
|
| "path-list"
|
|
@@ -373,8 +391,13 @@ export function describeShape(property: object): CoercionRow {
|
|
|
373
391
|
case "number":
|
|
374
392
|
case "integer": {
|
|
375
393
|
// Both bounds, as `numberProperty` writes them. A half-open interval is not a shape any
|
|
376
|
-
// Record declares, so it reads as a plain number and `run` owns the bound.
|
|
377
|
-
|
|
394
|
+
// Record declares, so it reads as a plain number and `run` owns the bound. A bounded integer
|
|
395
|
+
// Is the `bounded-number` row too, which holds it to integrality before the interval; only
|
|
396
|
+
// The BARE integer goes to `integerArg`, whose whole rule is integrality.
|
|
397
|
+
if (p.minimum !== undefined && p.maximum !== undefined) {
|
|
398
|
+
return "bounded-number";
|
|
399
|
+
}
|
|
400
|
+
return p.type === "integer" ? "integer" : "number";
|
|
378
401
|
}
|
|
379
402
|
case "string": {
|
|
380
403
|
return "string";
|
|
@@ -486,6 +509,9 @@ function shapeOf(property: PropertySchema): string {
|
|
|
486
509
|
case "number": {
|
|
487
510
|
return "a finite number";
|
|
488
511
|
}
|
|
512
|
+
case "integer": {
|
|
513
|
+
return "an integer";
|
|
514
|
+
}
|
|
489
515
|
case "string": {
|
|
490
516
|
return "a non-empty string";
|
|
491
517
|
}
|
|
@@ -577,11 +603,20 @@ function coerceProperty(
|
|
|
577
603
|
return booleanArg(commandId, args, key);
|
|
578
604
|
}
|
|
579
605
|
case "bounded-number": {
|
|
606
|
+
// Integrality first, so `{ type: "integer", minimum: 1, maximum: 9 }` refuses `1.5` by name
|
|
607
|
+
// Rather than admitting it as a number inside the interval — the static check's `type` row
|
|
608
|
+
// Already refused it, and the two passes must agree.
|
|
609
|
+
if (property.type === "integer") {
|
|
610
|
+
integerArg(commandId, args, key);
|
|
611
|
+
}
|
|
580
612
|
return boundedNumberArg(commandId, args, key, property.minimum!, property.maximum!);
|
|
581
613
|
}
|
|
582
614
|
case "number": {
|
|
583
615
|
return numberArg(commandId, args, key);
|
|
584
616
|
}
|
|
617
|
+
case "integer": {
|
|
618
|
+
return integerArg(commandId, args, key);
|
|
619
|
+
}
|
|
585
620
|
case "string": {
|
|
586
621
|
return stringArg(commandId, args, key);
|
|
587
622
|
}
|
package/src/commands/registry.ts
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
*
|
|
28
28
|
* The registry takes its context by injection (`getContext`), so nothing here imports a state
|
|
29
29
|
* module. That is what lets the CI checks import the command set in a bare Bun process and lets
|
|
30
|
-
* every test build the exact state it wants to assert against.
|
|
30
|
+
* every test build the exact state it wants to assert against. The one store it reaches is the
|
|
31
|
+
* notification store, through `run-reported.ts`, and only from `handleKeyEvent`: a chord's refusal
|
|
32
|
+
* has no caller to hand it to, so the registry files it where every other surface's refusal goes.
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
import { createKeymap } from "./keymap";
|
|
@@ -35,6 +37,7 @@ import type { KeyChordEvent, Keymap, KeymapMatch } from "./keymap";
|
|
|
35
37
|
import { checkRecordPlacements } from "./levels";
|
|
36
38
|
import type { Category, KeyScope, Level, Placement } from "./levels";
|
|
37
39
|
import { coerceArgs } from "./command-args";
|
|
40
|
+
import { runReported } from "./run-reported";
|
|
38
41
|
import type { CommandContext } from "./context";
|
|
39
42
|
|
|
40
43
|
/** Arguments as they arrive from a palette prompt, an automation manifest step or an AI tool call. */
|
|
@@ -435,13 +438,20 @@ export function createCommandRegistry(options: CommandRegistryOptions): CommandR
|
|
|
435
438
|
if (!registry.isEnabled(hit.commandId)) {
|
|
436
439
|
return hit.commandId;
|
|
437
440
|
}
|
|
438
|
-
/* An ARGUMENT refusal is the one
|
|
439
|
-
record with `{}`, and `run` coerces that against the
|
|
440
|
-
schema requires a key cannot be a chord — every shipped
|
|
441
|
-
`required: []` by the sweep in `tests/command-args.test.ts`, and a
|
|
442
|
-
override that binds one anyway hears about it as a
|
|
443
|
-
key, rather than as a chord that silently does nothing.
|
|
444
|
-
|
|
441
|
+
/* An ARGUMENT refusal is the one refusal that can still happen here, and it is REPORTED
|
|
442
|
+
rather than thrown. A chord runs its record with `{}`, and `run` coerces that against the
|
|
443
|
+
schema first, so a record whose schema requires a key cannot be a chord — every shipped
|
|
444
|
+
keybinding is held to `required: []` by the sweep in `tests/command-args.test.ts`, and a
|
|
445
|
+
user's own keymap override that binds one anyway hears about it as a Problems row naming
|
|
446
|
+
the command and the key, rather than as a chord that silently does nothing. It used to
|
|
447
|
+
leave as a `RangeError` out of the keydown listener, which named the same things to the
|
|
448
|
+
console and to nobody; `run-reported.ts` is what catches that shape, and the rejection a
|
|
449
|
+
`run` body may produce later, for every surface alike. A gate that closes BETWEEN the
|
|
450
|
+
`isEnabled` above and `run`'s own re-read is reported the same way, and that is a choice:
|
|
451
|
+
the silent claim above is for a chord the person can see is disabled, while a gate that
|
|
452
|
+
moved under the chord is state the person cannot see, and the `requires` sentence in
|
|
453
|
+
Problems is the one account of why nothing happened. */
|
|
454
|
+
void runReported(registry, hit.commandId, undefined, "Keyboard");
|
|
445
455
|
return hit.commandId;
|
|
446
456
|
},
|
|
447
457
|
context: options.getContext,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-reported.ts — run a command from a surface, and let a refusal land in Problems.
|
|
3
|
+
*
|
|
4
|
+
* `registry.run` refuses in two shapes, and a caller that catches one and not the other still lets
|
|
5
|
+
* a refusal out. It throws SYNCHRONOUSLY — `CommandUnavailableError` when the gate refuses, a
|
|
6
|
+
* `RangeError` when `coerceArgs` refuses the received record BEFORE `run` is entered, and whatever
|
|
7
|
+
* a synchronous `run` body throws — and it REJECTS when an async `run` body does. A bare `void
|
|
8
|
+
* registry.run(id, args)` lets the first shape out of the click, keydown or notice action it came
|
|
9
|
+
* from, and a `.catch` on the promise alone catches nothing when `run` has already thrown; a
|
|
10
|
+
* `Promise.resolve(registry.run(...))` around it is the same mistake with more characters, because
|
|
11
|
+
* the argument is evaluated first. Three surfaces had grown the same six-line try/catch in reply
|
|
12
|
+
* (the palette's `runCommand`, the Settings menu's `runRow` and the Languages panel's `runByName`)
|
|
13
|
+
* while ten others still ran bare, which is the drift issue 333 names: a refusal reached Problems
|
|
14
|
+
* or the console depending on which surface the person happened to be on.
|
|
15
|
+
*
|
|
16
|
+
* This is the one spelling. It catches both shapes, files the sentence in Problems — the tier
|
|
17
|
+
* `notify.error` chooses, and the right one: a refusal names something the person has to fix or a
|
|
18
|
+
* schema that drifted from its caller, and neither is a toast that may be taken away — and returns
|
|
19
|
+
* a promise that never rejects, so a caller may `void` it or `await` it without a `.catch`.
|
|
20
|
+
*
|
|
21
|
+
* A refusal is the whole message; a CRASH is not. `CommandUnavailableError` and `RangeError` are
|
|
22
|
+
* the two shapes the registry and `coerceArgs` refuse with, and each is a sentence written to be
|
|
23
|
+
* read, so the Problems row is complete. Anything else out of a `run` body — a `TypeError` on a
|
|
24
|
+
* node that was not there — is a bug, and its stack is the part that matters; the row still names
|
|
25
|
+
* it, and the error object also goes to the console, where the palette used to send everything and
|
|
26
|
+
* where a trace can be opened. Without that the helper would have made every surface quieter than
|
|
27
|
+
* the keydown listener it replaced, which reached `window.onerror`.
|
|
28
|
+
*
|
|
29
|
+
* Two spellings, one rule. {@link runReported} takes the registry as a PARAMETER: `editor/
|
|
30
|
+
* shortcuts.ts`, the block action bar and the registry's own `handleKeyEvent` are handed theirs,
|
|
31
|
+
* and a helper that resolved the active registry for them would silently do nothing wherever a
|
|
32
|
+
* test, or a second window kind, ran a registry that is not the published one.
|
|
33
|
+
* {@link runActiveReported} is the same call for a surface that was spelled `void
|
|
34
|
+
* activeRegistry()?.run(id, args)` — a footer row, a palette hit, a tab-strip chip — and it keeps
|
|
35
|
+
* that spelling's one property: before the bootstrap has published a registry there is nothing to
|
|
36
|
+
* run and nothing to report, exactly as the `?.` was silent.
|
|
37
|
+
*
|
|
38
|
+
* The sweep in `tests/run-reported.test.ts` walks `packages/studio/src` for any other `.run(` on a
|
|
39
|
+
* registry. Two callers own their refusal and are named there as such — `services/automation.ts`
|
|
40
|
+
* and `services/ai-command-tools.ts`, where the script or the model is the one who reads it — and
|
|
41
|
+
* the `?.run(` spelling issue 333 did not list survives in a RATCHET of files the sweep names one
|
|
42
|
+
* by one, each still holding a bare call; the list can only shrink, so a new surface cannot spell
|
|
43
|
+
* it bare and an existing one converted must drop its entry.
|
|
44
|
+
*
|
|
45
|
+
* `runReported` does not pre-check `isEnabled`. A surface that renders a disabled control for a
|
|
46
|
+
* refused record has already shown the `requires` sentence, and a click that arrives on one anyway
|
|
47
|
+
* (state moved between the render and the click) is a refusal the person should read, not a
|
|
48
|
+
* swallowed no-op. The callers that DO pre-check keep doing so for their own reasons — a chord on a
|
|
49
|
+
* disabled command is claimed silently by `handleKeyEvent`, by design — and route what remains
|
|
50
|
+
* through here.
|
|
51
|
+
*
|
|
52
|
+
* @docs studio/interface/problems-and-progress
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
56
|
+
import { notify } from "../services/notify";
|
|
57
|
+
import { activeRegistry } from "./active-registry";
|
|
58
|
+
import type { CommandArgs, CommandRegistry } from "./registry";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error is one of the two REFUSAL shapes, whose message is written to be read whole.
|
|
62
|
+
*
|
|
63
|
+
* By name rather than `instanceof CommandUnavailableError`: `registry.ts` imports this module for
|
|
64
|
+
* `handleKeyEvent`, and a value import back would close a cycle for one class whose constructor
|
|
65
|
+
* sets exactly this name. `RangeError` is the shape `coerceArgs` and a `run` body refuse with.
|
|
66
|
+
*/
|
|
67
|
+
function isRefusal(error: unknown): boolean {
|
|
68
|
+
return (
|
|
69
|
+
error instanceof RangeError ||
|
|
70
|
+
(error instanceof Error && error.name === "CommandUnavailableError")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run `id` with `args` on `registry`, and file whatever refuses it in Problems under `source`.
|
|
76
|
+
*
|
|
77
|
+
* `source` is the SURFACE the run came from ("Palette", "Keyboard", "Languages"), the column the
|
|
78
|
+
* Problems panel groups by; it defaults to the command id, which is what the Settings menu filed
|
|
79
|
+
* under before this helper existed and is still a true answer to "who reported it". The Problems
|
|
80
|
+
* row is keyed by the command id, so a chord held down on a refusing record is one problem rather
|
|
81
|
+
* than sixty — the file-watcher rule `services/notify.ts` writes down for `key`.
|
|
82
|
+
*
|
|
83
|
+
* The returned promise settles when the command does, and never rejects: an `await` on it is a
|
|
84
|
+
* wait, not a second place the refusal could escape from.
|
|
85
|
+
*/
|
|
86
|
+
export function runReported(
|
|
87
|
+
registry: CommandRegistry,
|
|
88
|
+
id: string,
|
|
89
|
+
args?: CommandArgs,
|
|
90
|
+
source: string = id,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const report = (error: unknown) => {
|
|
93
|
+
if (!isRefusal(error)) {
|
|
94
|
+
// A crash, not a refusal: the row below carries the sentence, the console keeps the stack.
|
|
95
|
+
console.error(`${source}: command "${id}" failed`, error);
|
|
96
|
+
}
|
|
97
|
+
notify.error(errorMessage(error), { key: `command.run:${id}`, source });
|
|
98
|
+
};
|
|
99
|
+
let result: void | Promise<void>;
|
|
100
|
+
try {
|
|
101
|
+
result = registry.run(id, args);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
report(error);
|
|
104
|
+
return Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
return Promise.resolve(result).catch(report);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* {@link runReported} on this window's registry, or nothing before the bootstrap has published one.
|
|
111
|
+
*
|
|
112
|
+
* The `?.` this replaces was the surfaces' answer to `active-registry.ts` returning `null` for the
|
|
113
|
+
* frame before the bootstrap composes the registry; a click that lands in that frame has no
|
|
114
|
+
* registry to refuse it, so there is no refusal to file. The resolved promise keeps the caller's
|
|
115
|
+
* `void` honest either way.
|
|
116
|
+
*/
|
|
117
|
+
export function runActiveReported(id: string, args?: CommandArgs, source?: string): Promise<void> {
|
|
118
|
+
const registry = activeRegistry();
|
|
119
|
+
return registry ? runReported(registry, id, args, source) : Promise.resolve();
|
|
120
|
+
}
|
package/src/editor/shortcuts.ts
CHANGED
|
@@ -77,7 +77,7 @@ import { clipboardCommands, pasteNode } from "./context-menu";
|
|
|
77
77
|
import { hasElementSelection, hasSelection, inCanvas, keyScopeStack } from "../commands/context";
|
|
78
78
|
import { defaultCommands } from "../commands/defaults";
|
|
79
79
|
import { setActiveRegistry } from "../commands/active-registry";
|
|
80
|
-
import {
|
|
80
|
+
import { runReported } from "../commands/run-reported";
|
|
81
81
|
import type { CommandContext } from "../commands/context";
|
|
82
82
|
import type { DockId as CommandDockId } from "../commands/defaults";
|
|
83
83
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
@@ -388,11 +388,12 @@ function toggleShellDock(registry: CommandRegistry, dock: CommandDockId): void {
|
|
|
388
388
|
*
|
|
389
389
|
* `view.setAssistant` and `view.setRightTab` are `shell.ts`'s, composed into the app's registry by
|
|
390
390
|
* the bootstrap — a reduced registry (a test, a future second window kind) may not carry them, and
|
|
391
|
-
* a missing id must be inert rather than an exception thrown out of a keydown handler.
|
|
391
|
+
* a missing id must be inert rather than an exception thrown out of a keydown handler. A present
|
|
392
|
+
* one that refuses its arguments is not inert: `commands/run-reported.ts` files it in Problems.
|
|
392
393
|
*/
|
|
393
394
|
function runIfPresent(registry: CommandRegistry, id: string, args: Record<string, unknown>): void {
|
|
394
395
|
if (registry.get(id) && registry.isEnabled(id)) {
|
|
395
|
-
void registry
|
|
396
|
+
void runReported(registry, id, args, "Keyboard");
|
|
396
397
|
}
|
|
397
398
|
}
|
|
398
399
|
|
|
@@ -901,34 +902,21 @@ export function registerStudioCommands(
|
|
|
901
902
|
*
|
|
902
903
|
* `preventDefault()` iff a command claimed the chord. A command that is VISIBLE but disabled — ⌘Z
|
|
903
904
|
* with nothing to undo, Delete on the document element — still counts as claiming it: the registry
|
|
904
|
-
*
|
|
905
|
-
* because the chord is spoken for and letting the browser act on it instead would
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
905
|
+
* consults `isEnabled` itself and returns the id without running, and swallowing the key is the
|
|
906
|
+
* honest outcome, because the chord is spoken for and letting the browser act on it instead would
|
|
907
|
+
* be a surprise. Nothing is caught here, because nothing leaves: `handleKeyEvent` runs the hit
|
|
908
|
+
* through `commands/run-reported.ts`, so a record that refuses its arguments, a gate that closed
|
|
909
|
+
* between the check and the run, or a `run` body that throws is a Problems row under "Keyboard"
|
|
910
|
+
* rather than an exception out of the keydown listener.
|
|
910
911
|
*/
|
|
911
912
|
function dispatchKey(registry: CommandRegistry, event: KeyboardEvent): string | undefined {
|
|
912
|
-
const commandId =
|
|
913
|
+
const commandId = registry.handleKeyEvent(event, keyScopeStack(registry.context()));
|
|
913
914
|
if (commandId) {
|
|
914
915
|
event.preventDefault();
|
|
915
916
|
}
|
|
916
917
|
return commandId;
|
|
917
918
|
}
|
|
918
919
|
|
|
919
|
-
/** The id of the command that claimed the chord, whether it ran or refused. */
|
|
920
|
-
function claimChord(registry: CommandRegistry, event: KeyboardEvent): string | undefined {
|
|
921
|
-
const stack = keyScopeStack(registry.context());
|
|
922
|
-
try {
|
|
923
|
-
return registry.handleKeyEvent(event, stack);
|
|
924
|
-
} catch (error) {
|
|
925
|
-
if (error instanceof CommandUnavailableError) {
|
|
926
|
-
return error.commandId;
|
|
927
|
-
}
|
|
928
|
-
throw error;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
920
|
// ─── Pointer and wheel gestures ───────────────────────────────────────────────
|
|
933
921
|
|
|
934
922
|
/**
|
package/src/files/components.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { getPlatform } from "../platform";
|
|
4
4
|
import { projectState } from "../store";
|
|
5
5
|
import type { ComponentMeta } from "../types";
|
|
6
|
+
import { componentMetaFrom } from "@jxsuite/schema/component-meta";
|
|
6
7
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
7
8
|
|
|
8
9
|
/** A discovered component: project components carry a file path; package components may not. */
|
|
@@ -26,6 +27,44 @@ export async function loadComponentRegistry() {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Bring the registry up to date with a document that was just saved.
|
|
32
|
+
*
|
|
33
|
+
* The registry is the snapshot the platform's scan took when the project opened, and a saved
|
|
34
|
+
* component definition used to leave it behind: give a prop `format: "image"` in the definition and
|
|
35
|
+
* every instance's Component Settings kept drawing the text box the snapshot knew, until the
|
|
36
|
+
* project was reopened. The saved document is the same input the scan reads, through the same
|
|
37
|
+
* extractor (`componentMetaFrom`, shared by all three backends), so the entry is rebuilt from it
|
|
38
|
+
* here rather than by a rescan of every JSON file in the project. A definition that stopped being a
|
|
39
|
+
* component (its `tagName` lost its hyphen) leaves the registry by the same rule the scan applies.
|
|
40
|
+
*
|
|
41
|
+
* @param {string | null} path - The saved document's project-relative path; `null` for an unsaved
|
|
42
|
+
* draft, which no scan would have seen either.
|
|
43
|
+
* @param {unknown} doc - The saved document.
|
|
44
|
+
* @returns {boolean} Whether the registry changed, so a caller knows to repaint what reads it.
|
|
45
|
+
*/
|
|
46
|
+
export function noteComponentSaved(path: string | null, doc: unknown): boolean {
|
|
47
|
+
if (!path) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const index = componentRegistry.findIndex((c) => c.path === path);
|
|
51
|
+
const meta = componentMetaFrom(doc, path);
|
|
52
|
+
if (!meta) {
|
|
53
|
+
if (index === -1) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
componentRegistry.splice(index, 1);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const entry = meta as ComponentEntry;
|
|
60
|
+
if (index === -1) {
|
|
61
|
+
componentRegistry.push(entry);
|
|
62
|
+
} else {
|
|
63
|
+
componentRegistry[index] = entry;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
/**
|
|
30
69
|
* Build a fresh instance definition for a component: $props from prop defaults, plus the
|
|
31
70
|
* component's slot fallback children cloned in as starting content so editors can edit from the
|
package/src/files/files.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
61
61
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
62
62
|
import {
|
|
63
|
+
PRIMARY_PANE,
|
|
63
64
|
activateTab,
|
|
64
65
|
moveTabToPane,
|
|
65
66
|
openTab,
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
import { openCsvGridTab, openPagesGrid } from "../grid/grid-open";
|
|
74
75
|
import { isViewableMedia, openMediaTab } from "../media/media-open";
|
|
75
76
|
import { activeRegistry } from "../commands/active-registry";
|
|
77
|
+
import { runActiveReported, runReported } from "../commands/run-reported";
|
|
76
78
|
import { collectionOfPath } from "../content/entry-model";
|
|
77
79
|
import { confirmFileDelete, parseSourceForPath, renamePromptMessage } from "./file-ops";
|
|
78
80
|
import { parseJsonDocument } from "./json-layout";
|
|
@@ -404,7 +406,7 @@ export async function openLastSessionOrHome(): Promise<boolean> {
|
|
|
404
406
|
if (session) {
|
|
405
407
|
const opened = await restoreSession(session, {
|
|
406
408
|
ensureSecondPane: () => {
|
|
407
|
-
receivingPane();
|
|
409
|
+
receivingPane(PRIMARY_PANE);
|
|
408
410
|
},
|
|
409
411
|
openFile: (path, paneId) => openFileInTab(path, { focus: false, paneId }),
|
|
410
412
|
});
|
|
@@ -1524,6 +1526,11 @@ function fileRowFacts(entry: { path: string; type: string }): Record<string, unk
|
|
|
1524
1526
|
}
|
|
1525
1527
|
return facts;
|
|
1526
1528
|
}
|
|
1529
|
+
/* Every file row states `file`, because `document.openToSide` takes one and the row is the only
|
|
1530
|
+
thing that knows it — the same reason `content.openEntry`'s `path` is stated here. A command
|
|
1531
|
+
whose required arguments this row cannot answer is skipped, so without this fact the
|
|
1532
|
+
"Open to the Side" row would never render on any file. */
|
|
1533
|
+
facts.file = entry.path;
|
|
1527
1534
|
if (collectionOfPath(entry.path)) {
|
|
1528
1535
|
facts.path = entry.path;
|
|
1529
1536
|
}
|
|
@@ -1582,7 +1589,7 @@ function placedFileRows(entry: { path: string; type: string }): MenuRowProjectio
|
|
|
1582
1589
|
rows.push(
|
|
1583
1590
|
reason === undefined
|
|
1584
1591
|
? treeMenuRow(command.id, command.title, () => {
|
|
1585
|
-
void registry
|
|
1592
|
+
void runReported(registry, command.id, args, "Files");
|
|
1586
1593
|
})
|
|
1587
1594
|
: {
|
|
1588
1595
|
destructive: false,
|
|
@@ -2237,23 +2244,36 @@ export interface OpenFileOpts {
|
|
|
2237
2244
|
/**
|
|
2238
2245
|
* Bring an ALREADY-OPEN tab to where the caller asked for it.
|
|
2239
2246
|
*
|
|
2240
|
-
* Three cases, and the third is the one a
|
|
2247
|
+
* Three cases, and the third is the one a cross-pane open depends on:
|
|
2241
2248
|
*
|
|
2242
|
-
* | the tab is…
|
|
2243
|
-
* |
|
|
2244
|
-
* |
|
|
2245
|
-
* | elsewhere, and **
|
|
2246
|
-
* |
|
|
2249
|
+
* | the tab is… | behaviour |
|
|
2250
|
+
* | ---------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
2251
|
+
* | elsewhere, and **not** its pane's active tab | move it — one tab is one document in one strip |
|
|
2252
|
+
* | elsewhere, and **is** its pane's active tab, browsing (`focus: false`) | **nothing.** You are already looking at it |
|
|
2253
|
+
* | already in the requested pane, or committing (moving/staying, focus follows) | activate it there, honouring `focus` |
|
|
2247
2254
|
*
|
|
2248
|
-
* The
|
|
2255
|
+
* The first exists because moving it would oscillate: a derivation that re-resolves to the document
|
|
2249
2256
|
* the author is editing would yank it out of their pane and into the assistant one, and the follow
|
|
2250
2257
|
* would then re-resolve against whatever landed in its place.
|
|
2258
|
+
*
|
|
2259
|
+
* The third is the first's other half, and it is what "Open to the Side" needs: the tab is
|
|
2260
|
+
* elsewhere and IS its pane's active tab, but the caller asked for it in a DIFFERENT pane with the
|
|
2261
|
+
* focus following — the author asked to go there, so "you are already looking at it" is the wrong
|
|
2262
|
+
* answer. The move is a `moveTabToPane`, which activates nothing and moves no focus; the
|
|
2263
|
+
* `activateTab` below then points the keyboard at the pane it landed in.
|
|
2264
|
+
*
|
|
2265
|
+
* **The guard reads `wanted !== undefined && holder` — not also `holder.id !== wanted`.** A tab
|
|
2266
|
+
* already in the requested pane still passes through: `moveTabToPane` is idempotent for a tab
|
|
2267
|
+
* already in `paneId` (its own doc, next to this call), so entering the block and calling it anyway
|
|
2268
|
+
* costs nothing and reaches the identical final `activateTab`. Naming the extra condition would ask
|
|
2269
|
+
* a reader to verify a distinction that changes nothing — the redundant branch this replaced is
|
|
2270
|
+
* `check-lens-mutants.ts`'s own "delete rather than assert" rule, applied to itself.
|
|
2251
2271
|
*/
|
|
2252
2272
|
function revealOpenTab(tabId: string, opts: OpenFileOpts): void {
|
|
2253
2273
|
const wanted = opts.paneId;
|
|
2254
2274
|
const holder = paneOfTab(tabId);
|
|
2255
|
-
if (wanted !== undefined && holder
|
|
2256
|
-
if (holder.activeTabId === tabId) {
|
|
2275
|
+
if (wanted !== undefined && holder) {
|
|
2276
|
+
if (holder.activeTabId === tabId && opts.focus === false) {
|
|
2257
2277
|
return;
|
|
2258
2278
|
}
|
|
2259
2279
|
moveTabToPane(tabId, wanted);
|
|
@@ -2292,7 +2312,7 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2292
2312
|
* why a branch anywhere else would send a restored media tab straight back down the error path.
|
|
2293
2313
|
*/
|
|
2294
2314
|
if (isViewableMedia(path)) {
|
|
2295
|
-
openMediaTab(path);
|
|
2315
|
+
openMediaTab(path, opts);
|
|
2296
2316
|
if (follows) {
|
|
2297
2317
|
requireProjectState().selectedPath = path;
|
|
2298
2318
|
}
|
|
@@ -2307,8 +2327,10 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2307
2327
|
// CSV files open in the grid editor (source mode remains as the raw-text alternate).
|
|
2308
2328
|
if (path.toLowerCase().endsWith(".csv")) {
|
|
2309
2329
|
try {
|
|
2310
|
-
await openCsvGridTab(path);
|
|
2311
|
-
|
|
2330
|
+
await openCsvGridTab(path, opts);
|
|
2331
|
+
if (follows) {
|
|
2332
|
+
requireProjectState().selectedPath = path;
|
|
2333
|
+
}
|
|
2312
2334
|
trackRecentFile({
|
|
2313
2335
|
name: path.split("/").pop() || path,
|
|
2314
2336
|
path,
|
|
@@ -2394,8 +2416,10 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2394
2416
|
* Open a file into a NAMED pane, browsing rather than committing, leaving the keyboard behind.
|
|
2395
2417
|
*
|
|
2396
2418
|
* The same body as {@link openFileInTab} with the three options a side-open always wants, given a
|
|
2397
|
-
* signature that says which pane in the first parameter. It is what
|
|
2398
|
-
*
|
|
2419
|
+
* signature that says which pane in the first parameter. It is what the READS mean by "open it
|
|
2420
|
+
* beside this" — following a layout, `pane.compareWith`. The gestures that open something
|
|
2421
|
+
* (drill-in, Open to the Side) call {@link openFileInTab} directly, because the pane they open into
|
|
2422
|
+
* takes the keyboard.
|
|
2399
2423
|
*
|
|
2400
2424
|
* @param {string} paneId
|
|
2401
2425
|
* @param {string} path
|
|
@@ -2498,7 +2522,7 @@ const FILE_ACTIONS: FilesPanelActions = {
|
|
|
2498
2522
|
/* The DECLARED command, run through the registry — never a second opener of this surface's own
|
|
2499
2523
|
(§12.5). It is the one the ⌘O chord, the palette and the status bar's PROJECT field already
|
|
2500
2524
|
run, and it is the only one that knows about the picker modal and the "new window" target. */
|
|
2501
|
-
void
|
|
2525
|
+
void runActiveReported("project.open", undefined, "Files");
|
|
2502
2526
|
},
|
|
2503
2527
|
refresh: () => {
|
|
2504
2528
|
void refreshFileTree();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy grid engine.
|
|
3
|
+
*
|
|
4
|
+
* `grid-view.ts` is the only module that imports `tabulator-tables`, and as of 6.5.3 Tabulator's
|
|
5
|
+
* `Row` class runs `document.createElement` in a static field, so the CLASS DEFINITION — not the
|
|
6
|
+
* table's construction — needs a DOM to exist. That makes `grid-view.ts` unsafe to reach with a
|
|
7
|
+
* static value-import from any module that must load with no DOM: `app-commands.ts` pulls
|
|
8
|
+
* `gridViewCommands` from `grid-panel.ts` for the command set `scripts/check-command-levels.ts` and
|
|
9
|
+
* its siblings read in a bare Bun process (see `commands/defaults.ts`'s note on
|
|
10
|
+
* `selection.convertToComponent` for the sibling convention this follows).
|
|
11
|
+
*
|
|
12
|
+
* So `grid-panel.ts` never has a static value-import of `./grid-view`; it loads the engine through
|
|
13
|
+
* here instead, exactly as `services/monaco-lazy.ts` defers Monaco. Real usage pays no cost for it
|
|
14
|
+
* — a grid tab cannot exist before `document` does — and the two accessors mirror Monaco's: one for
|
|
15
|
+
* code that MOUNTS a grid (async is free there), one for code that can only run with a grid engine
|
|
16
|
+
* already loaded.
|
|
17
|
+
*/
|
|
18
|
+
import type * as gridViewModule from "./grid-view";
|
|
19
|
+
|
|
20
|
+
export type GridEngine = typeof gridViewModule;
|
|
21
|
+
|
|
22
|
+
let _engine: GridEngine | null = null;
|
|
23
|
+
let _loading: Promise<GridEngine> | null = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load the Tabulator-backed grid engine. Memoized — concurrent callers share one import.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Promise<GridEngine>}
|
|
29
|
+
*/
|
|
30
|
+
export function loadGridEngine(): Promise<GridEngine> {
|
|
31
|
+
if (_engine) {
|
|
32
|
+
return Promise.resolve(_engine);
|
|
33
|
+
}
|
|
34
|
+
// Not `async`: an async function wraps the memo in a fresh promise per call, so concurrent
|
|
35
|
+
// Callers could not observe that they share one in-flight load.
|
|
36
|
+
_loading ??= import("./grid-view").then((engine) => {
|
|
37
|
+
_engine = engine;
|
|
38
|
+
return engine;
|
|
39
|
+
});
|
|
40
|
+
return _loading;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The loaded engine, or null before the first grid mount.
|
|
45
|
+
*
|
|
46
|
+
* For call sites that can run synchronously once a grid already exists in the session — they must
|
|
47
|
+
* not force the module to load, only read it if it is already there.
|
|
48
|
+
*
|
|
49
|
+
* @returns {GridEngine | null}
|
|
50
|
+
*/
|
|
51
|
+
export function loadedGridEngine(): GridEngine | null {
|
|
52
|
+
return _engine;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reset the memo (tests only). */
|
|
56
|
+
export function resetGridLazy(): void {
|
|
57
|
+
_engine = null;
|
|
58
|
+
_loading = null;
|
|
59
|
+
}
|