@checkstack/ui 1.11.0 → 1.13.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/.storybook/main.ts +43 -0
- package/CHANGELOG.md +326 -0
- package/package.json +23 -18
- package/scripts/generate-stdlib-types.ts +23 -0
- package/src/components/Accordion.tsx +17 -9
- package/src/components/ActionCard.tsx +99 -11
- package/src/components/BrandIcon.tsx +57 -0
- package/src/components/CodeEditor/CodeEditor.tsx +159 -14
- package/src/components/CodeEditor/TypefoxEditor.tsx +537 -168
- package/src/components/CodeEditor/editorTheme.test.ts +41 -0
- package/src/components/CodeEditor/editorTheme.ts +26 -0
- package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
- package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
- package/src/components/CodeEditor/importSpecifiers.ts +267 -0
- package/src/components/CodeEditor/index.ts +26 -0
- package/src/components/CodeEditor/monacoGuard.ts +76 -0
- package/src/components/CodeEditor/monacoTsService.ts +185 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptContext.test.ts +15 -7
- package/src/components/CodeEditor/scriptContext.ts +12 -18
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/types.ts +79 -0
- package/src/components/CodeEditor/validateScripts.ts +172 -0
- package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
- package/src/components/ConfirmationModal.tsx +7 -1
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +119 -47
- package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
- package/src/components/DynamicForm/FormField.tsx +183 -15
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +78 -2
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +20 -0
- package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
- package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
- package/src/components/DynamicForm/types.ts +134 -1
- package/src/components/DynamicForm/utils.test.ts +38 -0
- package/src/components/DynamicForm/utils.ts +54 -0
- package/src/components/DynamicForm/validation.logic.test.ts +255 -0
- package/src/components/DynamicForm/validation.logic.ts +210 -0
- package/src/components/DynamicIcon.tsx +39 -17
- package/src/components/Markdown.tsx +68 -2
- package/src/components/Popover.tsx +6 -1
- package/src/components/ScriptTestPanel.logic.test.ts +139 -0
- package/src/components/ScriptTestPanel.logic.ts +137 -0
- package/src/components/ScriptTestPanel.tsx +394 -0
- package/src/components/Sheet.tsx +21 -6
- package/src/components/Spinner.tsx +56 -0
- package/src/components/StatusBadge.tsx +78 -0
- package/src/components/StrategyConfigCard.tsx +3 -3
- package/src/components/Tabs.tsx +7 -1
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/UserMenu.logic.test.ts +37 -0
- package/src/components/UserMenu.logic.ts +30 -0
- package/src/components/UserMenu.tsx +40 -12
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/iconRegistry.tsx +27 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/index.ts +7 -0
- package/stories/ActionCard.stories.tsx +60 -0
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/Introduction.mdx +1 -1
- package/stories/Markdown.stories.tsx +56 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/Spinner.stories.tsx +90 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/tsconfig.json +4 -0
|
@@ -19,16 +19,21 @@
|
|
|
19
19
|
// (Monarch grammars for ~80 languages) without pulling in the extension host
|
|
20
20
|
// (no SharedArrayBuffer / COOP / COEP needed).
|
|
21
21
|
import "@codingame/monaco-vscode-standalone-languages";
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// TS/JS language-service setup (compiler options, ambient stdlib types, worker
|
|
23
|
+
// factory) lives in the shared `monacoTsService` module so the headless
|
|
24
|
+
// `validateScripts` validator can reuse the exact same configured singletons.
|
|
25
25
|
import {
|
|
26
26
|
typescriptDefaults,
|
|
27
27
|
javascriptDefaults,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
ensureStandaloneWorkerFactory,
|
|
29
|
+
} from "./monacoTsService";
|
|
30
|
+
import {
|
|
31
|
+
areVscodeServicesReady,
|
|
32
|
+
claimColdInit,
|
|
33
|
+
markVscodeServicesReady,
|
|
34
|
+
onVscodeServicesReady,
|
|
35
|
+
releaseColdInit,
|
|
36
|
+
} from "./vscodeServicesSignal";
|
|
32
37
|
// Named import also triggers the side-effect registration of the REAL VS Code
|
|
33
38
|
// JSON language service (proper highlighting + completion + folding), replacing
|
|
34
39
|
// the hand-rolled `json-template` Monarch grammar. We turn its built-in
|
|
@@ -41,21 +46,25 @@ import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-
|
|
|
41
46
|
// `languageStatusServiceOverride` below).
|
|
42
47
|
import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
|
|
50
|
+
import {
|
|
51
|
+
type CSSProperties,
|
|
52
|
+
useEffect,
|
|
53
|
+
useId,
|
|
54
|
+
useLayoutEffect,
|
|
55
|
+
useRef,
|
|
56
|
+
useState,
|
|
57
|
+
} from "react";
|
|
58
|
+
// Types come from the package directly (type-only, so it pulls no runtime code
|
|
59
|
+
// and the lint rule allows it). RUNTIME monaco access goes through the guarded
|
|
60
|
+
// accessor (`monacoRuntime`, see monacoGuard.ts): in dev it throws if a
|
|
61
|
+
// `monaco.editor.*` / `monaco.languages.*` function runs before the services
|
|
62
|
+
// are initialized, preventing the "Services are already initialized" regression
|
|
63
|
+
// class. `no-restricted-imports` forbids importing the raw editor-api value.
|
|
64
|
+
import type * as monaco from "@codingame/monaco-vscode-editor-api";
|
|
65
|
+
import { monaco as monacoRuntime } from "./monacoGuard";
|
|
66
|
+
import { useTheme } from "../ThemeProvider";
|
|
67
|
+
import { MONACO_THEME_MAP, VARIABLE_TOKEN_COLOR } from "./editorTheme";
|
|
59
68
|
import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
|
|
60
69
|
import { extractBracketKeyGroups } from "./bracketKeyGroups";
|
|
61
70
|
import { validateJsonTemplate } from "./validateJsonTemplate";
|
|
@@ -68,131 +77,152 @@ import {
|
|
|
68
77
|
matchShellEnvVarTrigger,
|
|
69
78
|
} from "./shellEnvVarMatcher";
|
|
70
79
|
import type {
|
|
80
|
+
AcquireTypes,
|
|
71
81
|
CodeEditorLanguage,
|
|
72
82
|
EditorMarker,
|
|
73
83
|
ShellEnvVar,
|
|
74
84
|
TemplateProperty,
|
|
75
85
|
} from "./types";
|
|
86
|
+
import {
|
|
87
|
+
importSpecifierCompletionContext,
|
|
88
|
+
mergeImportCompletionEntries,
|
|
89
|
+
parseBareImportSpecifiers,
|
|
90
|
+
planAcquisitions,
|
|
91
|
+
} from "./importSpecifiers";
|
|
92
|
+
// Authoritative, build-time-derived list of importable runtime built-in
|
|
93
|
+
// specifiers (`node:fs`, bare `fs`, `bun`, `bun:test`, ...). Generated from the
|
|
94
|
+
// SAME bundled `@types/node` + `bun-types` declarations the editor injects (see
|
|
95
|
+
// scripts/generate-stdlib-types.ts -> extractBuiltinModuleSpecifiers), so the
|
|
96
|
+
// import-name completions never drift from the runtime stdlib. Imported as a
|
|
97
|
+
// plain JSON module (tiny: ~115 names); the bulky type bodies stay in the
|
|
98
|
+
// separately code-split stdlib-types.json.
|
|
99
|
+
import builtinModulesJson from "./generated/builtin-modules.json";
|
|
76
100
|
import {
|
|
77
101
|
type EditorAppConfig,
|
|
78
102
|
type TextContents,
|
|
79
103
|
} from "monaco-languageclient/editorApp";
|
|
80
104
|
import { type MonacoVscodeApiConfig } from "monaco-languageclient/vscodeApiWrapper";
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const editorWorkerLoader: WorkerLoader = () =>
|
|
98
|
-
new Worker(editorWorkerUrl, { type: "module" });
|
|
99
|
-
|
|
100
|
-
const tsWorkerLoader: WorkerLoader = () =>
|
|
101
|
-
new Worker(tsWorkerUrl, { type: "module" });
|
|
102
|
-
|
|
103
|
-
const jsonWorkerLoader: WorkerLoader = () =>
|
|
104
|
-
new Worker(jsonWorkerUrl, { type: "module" });
|
|
105
|
+
// ─── Lazy Automatic Type Acquisition (ATA) registry ─────────────────────────
|
|
106
|
+
//
|
|
107
|
+
// The TS/JS language services are singletons, so acquired package types are
|
|
108
|
+
// registered ONCE and shared across every editor instance (a package imported
|
|
109
|
+
// in one script editor is then typed in all of them — harmless, since the
|
|
110
|
+
// declarations are the same install). State is module-scoped:
|
|
111
|
+
//
|
|
112
|
+
// - `acquiredFilePaths`: virtual paths already passed to addExtraLib (dedupe
|
|
113
|
+
// so two editors importing the same package don't double-register a file).
|
|
114
|
+
// - `acquiredSpecifiers`: package names already acquired (skip the fetch).
|
|
115
|
+
// - `acquireResetKey`: the install identity (lockfile hash) the current
|
|
116
|
+
// acquired-set belongs to; when it changes, the set is reset so types
|
|
117
|
+
// refresh against the new install.
|
|
118
|
+
const acquiredFilePaths = new Set<string>();
|
|
119
|
+
const acquiredSpecifiers = new Set<string>();
|
|
120
|
+
let currentAcquireResetKey: string | undefined;
|
|
105
121
|
|
|
106
122
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* it is called from module scope here, never from a component render.
|
|
123
|
+
* Reset the acquired-set when the install identity changes. The already-
|
|
124
|
+
* registered extra-libs are left in place (disposing them is unnecessary —
|
|
125
|
+
* the new install re-registers the same virtual paths, and addExtraLib
|
|
126
|
+
* overwrites by path), but the specifier set clears so each package is
|
|
127
|
+
* re-fetched against the new hash.
|
|
113
128
|
*/
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
editorWorkerService: editorWorkerLoader,
|
|
120
|
-
// Both must be defined or the worker factory errors (see upstream
|
|
121
|
-
// helper-classic.ts).
|
|
122
|
-
javascript: tsWorkerLoader,
|
|
123
|
-
typescript: tsWorkerLoader,
|
|
124
|
-
json: jsonWorkerLoader,
|
|
125
|
-
},
|
|
126
|
-
logger,
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// Base compiler options for the standalone TS + JS services. `types` (node +
|
|
131
|
-
// bun-types) is added only once the stdlib bundle has loaded (see
|
|
132
|
-
// ensureStandaloneStdlib), so the service doesn't transiently error on a
|
|
133
|
-
// missing `node` type while the ~3 MB bundle is still fetching.
|
|
134
|
-
const BASE_COMPILER_OPTIONS = {
|
|
135
|
-
target: ScriptTarget.ESNext,
|
|
136
|
-
module: ModuleKind.ESNext,
|
|
137
|
-
moduleResolution: ModuleResolutionKind.NodeJs,
|
|
138
|
-
lib: ["esnext"],
|
|
139
|
-
allowNonTsExtensions: false,
|
|
140
|
-
noEmit: true,
|
|
141
|
-
strict: true,
|
|
142
|
-
esModuleInterop: true,
|
|
129
|
+
const syncAcquireResetKey = (resetKey: string | undefined): void => {
|
|
130
|
+
if (resetKey === currentAcquireResetKey) return;
|
|
131
|
+
currentAcquireResetKey = resetKey;
|
|
132
|
+
acquiredSpecifiers.clear();
|
|
133
|
+
acquiredFilePaths.clear();
|
|
143
134
|
};
|
|
144
135
|
|
|
145
136
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* start the service with stale defaults - the timing race the legacy monaco
|
|
150
|
-
* editor hit.
|
|
137
|
+
* Register one acquired package's declaration files with both the TS and JS
|
|
138
|
+
* services, deduped by virtual path. Paths are `node_modules/...`-relative;
|
|
139
|
+
* we mount each at `file:///<path>` so NodeJs + `@types` resolution finds it.
|
|
151
140
|
*/
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
141
|
+
const registerAcquiredFiles = (
|
|
142
|
+
files: ReadonlyArray<{ path: string; content: string }>,
|
|
143
|
+
): void => {
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const uri = `file:///${file.path}`;
|
|
146
|
+
if (acquiredFilePaths.has(uri)) continue;
|
|
147
|
+
acquiredFilePaths.add(uri);
|
|
148
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
149
|
+
defaults.addExtraLib(file.content, uri);
|
|
150
|
+
}
|
|
161
151
|
}
|
|
162
152
|
};
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
// ─── @checkstack/sdk editor-type injection ──────────────────────────────────
|
|
155
|
+
//
|
|
156
|
+
// The running release's SDK editor bundle (ambient `.d.ts` for the script
|
|
157
|
+
// helpers + typed client) is mounted ONCE into the shared TS/JS services,
|
|
158
|
+
// keyed by release version. A deployment upgrade changes the key, so the libs
|
|
159
|
+
// reset and the editor never serves stale SDK types (plan §6.2).
|
|
160
|
+
const sdkMountedPaths = new Set<string>();
|
|
161
|
+
let currentSdkResetKey: string | undefined;
|
|
165
162
|
|
|
166
163
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* `monacoStdlib.ts` (without its `@monaco-editor/react` dependency). Runs at
|
|
172
|
-
* module load; this file is browser-only so the dynamic import is safe here.
|
|
164
|
+
* Mount the SDK bundle files, resetting on a release-version change. addExtraLib
|
|
165
|
+
* overwrites by path, so a version bump re-mounts the same virtual paths with
|
|
166
|
+
* fresh content; the mounted-path set just dedupes within a version so two
|
|
167
|
+
* editors don't double-register.
|
|
173
168
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
const mountSdkTypes = ({
|
|
170
|
+
files,
|
|
171
|
+
resetKey,
|
|
172
|
+
}: {
|
|
173
|
+
files: ReadonlyArray<{ path: string; content: string }>;
|
|
174
|
+
resetKey: string | undefined;
|
|
175
|
+
}): void => {
|
|
176
|
+
if (resetKey !== currentSdkResetKey) {
|
|
177
|
+
currentSdkResetKey = resetKey;
|
|
178
|
+
sdkMountedPaths.clear();
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const uri = `file:///${file.path}`;
|
|
182
|
+
// On a fresh version we re-mount (overwrite) even if the path was seen
|
|
183
|
+
// under a prior key; within a version, skip an already-mounted path.
|
|
184
|
+
if (sdkMountedPaths.has(uri)) continue;
|
|
185
|
+
sdkMountedPaths.add(uri);
|
|
186
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
187
|
+
defaults.addExtraLib(file.content, uri);
|
|
185
188
|
}
|
|
186
|
-
// The @types/node + bun-types declarations now exist at their node_modules
|
|
187
|
-
// virtual paths, so include them ambiently.
|
|
188
|
-
defaults.setCompilerOptions({
|
|
189
|
-
...BASE_COMPILER_OPTIONS,
|
|
190
|
-
types: ["node", "bun-types"],
|
|
191
|
-
});
|
|
192
189
|
}
|
|
193
190
|
};
|
|
194
191
|
|
|
195
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Acquire types for every NEW bare specifier in `source`, against the given
|
|
194
|
+
* resolver. Pure planning (`parseBareImportSpecifiers` / `planAcquisitions`)
|
|
195
|
+
* is unit-tested; this thin async glue is intentionally untested (no DOM /
|
|
196
|
+
* network in unit tests). A specifier is marked acquired even when it returns
|
|
197
|
+
* no files, so a typeless package isn't re-fetched on every keystroke.
|
|
198
|
+
*/
|
|
199
|
+
const runTypeAcquisition = async ({
|
|
200
|
+
source,
|
|
201
|
+
acquireTypes,
|
|
202
|
+
resetKey,
|
|
203
|
+
}: {
|
|
204
|
+
source: string;
|
|
205
|
+
acquireTypes: AcquireTypes;
|
|
206
|
+
resetKey: string | undefined;
|
|
207
|
+
}): Promise<void> => {
|
|
208
|
+
syncAcquireResetKey(resetKey);
|
|
209
|
+
const specifiers = parseBareImportSpecifiers(source);
|
|
210
|
+
const toAcquire = planAcquisitions({
|
|
211
|
+
specifiers,
|
|
212
|
+
acquired: acquiredSpecifiers,
|
|
213
|
+
});
|
|
214
|
+
for (const specifier of toAcquire) {
|
|
215
|
+
// Mark first so concurrent/keystroke re-runs don't double-fetch.
|
|
216
|
+
acquiredSpecifiers.add(specifier);
|
|
217
|
+
try {
|
|
218
|
+
const files = await acquireTypes(specifier);
|
|
219
|
+
registerAcquiredFiles(files);
|
|
220
|
+
} catch {
|
|
221
|
+
// A failed fetch un-marks so a later edit can retry.
|
|
222
|
+
acquiredSpecifiers.delete(specifier);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
196
226
|
|
|
197
227
|
// Turn OFF the JSON service's built-in validation. The editor content is a
|
|
198
228
|
// template that renders to JSON, so we validate the template-substituted form
|
|
@@ -242,20 +272,31 @@ const TEMPLATE_VALIDATORS: Partial<
|
|
|
242
272
|
// Variable-like tokens (`{{ template }}` expressions, shell `$env` refs) are
|
|
243
273
|
// highlighted via inline decorations rather than per-language grammars: this
|
|
244
274
|
// works for any language (yaml / xml / markdown have no template grammar; shell
|
|
245
|
-
// doesn't color `$VAR` inside strings) and keeps the color consistent.
|
|
246
|
-
// is
|
|
247
|
-
//
|
|
275
|
+
// doesn't color `$VAR` inside strings) and keeps the color consistent.
|
|
276
|
+
// The style element is injected once and updated whenever the resolved theme
|
|
277
|
+
// changes so the decoration tracks the editor theme.
|
|
248
278
|
const VARIABLE_TOKEN_CLASS = "checkstack-editor-variable";
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
279
|
+
const VARIABLE_TOKEN_STYLE_ID = "checkstack-editor-variable-style";
|
|
280
|
+
|
|
281
|
+
const ensureVariableTokenStyle = ({
|
|
282
|
+
resolvedTheme,
|
|
283
|
+
}: {
|
|
284
|
+
resolvedTheme: "light" | "dark";
|
|
285
|
+
}): void => {
|
|
286
|
+
const color = VARIABLE_TOKEN_COLOR[resolvedTheme];
|
|
287
|
+
const existing = document.querySelector<HTMLStyleElement>(
|
|
288
|
+
`#${VARIABLE_TOKEN_STYLE_ID}`,
|
|
289
|
+
);
|
|
290
|
+
if (existing !== null) {
|
|
291
|
+
// Update in place so a theme toggle refreshes the color.
|
|
292
|
+
existing.textContent = `.${VARIABLE_TOKEN_CLASS}{color:${color} !important;}`;
|
|
252
293
|
return;
|
|
253
294
|
}
|
|
254
|
-
variableTokenStyleInjected = true;
|
|
255
295
|
const style = document.createElement("style");
|
|
296
|
+
style.id = VARIABLE_TOKEN_STYLE_ID;
|
|
256
297
|
// `!important` so the decoration color always wins over the underlying token
|
|
257
298
|
// color (.mtkN), making `{{ }}` look identical inside and outside strings.
|
|
258
|
-
style.textContent = `.${VARIABLE_TOKEN_CLASS}{color
|
|
299
|
+
style.textContent = `.${VARIABLE_TOKEN_CLASS}{color:${color} !important;}`;
|
|
259
300
|
document.head.append(style);
|
|
260
301
|
};
|
|
261
302
|
|
|
@@ -271,7 +312,6 @@ const installRegexDecorations = ({
|
|
|
271
312
|
pattern: RegExp;
|
|
272
313
|
className: string;
|
|
273
314
|
}): monaco.IDisposable => {
|
|
274
|
-
ensureVariableTokenStyle();
|
|
275
315
|
const compute = (): monaco.editor.IModelDeltaDecoration[] => {
|
|
276
316
|
const text = model.getValue();
|
|
277
317
|
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
|
@@ -281,7 +321,7 @@ const installRegexDecorations = ({
|
|
|
281
321
|
const start = model.getPositionAt(match.index);
|
|
282
322
|
const end = model.getPositionAt(match.index + match[0].length);
|
|
283
323
|
decorations.push({
|
|
284
|
-
range: new
|
|
324
|
+
range: new monacoRuntime.Range(
|
|
285
325
|
start.lineNumber,
|
|
286
326
|
start.column,
|
|
287
327
|
end.lineNumber,
|
|
@@ -312,7 +352,7 @@ const installRegexDecorations = ({
|
|
|
312
352
|
const findModelById = (
|
|
313
353
|
modelId: string,
|
|
314
354
|
): monaco.editor.ITextModel | undefined =>
|
|
315
|
-
|
|
355
|
+
monacoRuntime.editor
|
|
316
356
|
.getModels()
|
|
317
357
|
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
318
358
|
|
|
@@ -327,6 +367,13 @@ export type TypefoxEditorProps = {
|
|
|
327
367
|
language?: CodeEditorLanguage;
|
|
328
368
|
/** Minimum editor height in pixels. Defaults to 240. */
|
|
329
369
|
minHeight?: number;
|
|
370
|
+
/**
|
|
371
|
+
* When true, the editor container fills its flex parent (`height: 100%`)
|
|
372
|
+
* instead of using a fixed `minHeight` px height, so it grows to fit a tall
|
|
373
|
+
* flex column (e.g. the popout dialog body). `minHeight` is still applied as
|
|
374
|
+
* a floor. Defaults to false, preserving the inline fixed-height behaviour.
|
|
375
|
+
*/
|
|
376
|
+
fillHeight?: boolean;
|
|
330
377
|
/**
|
|
331
378
|
* Generated ambient type definitions (the `context.d.ts`) injected as a TS
|
|
332
379
|
* extra-lib so `context.*` resolves with real fields. Wired up once per
|
|
@@ -356,11 +403,56 @@ export type TypefoxEditorProps = {
|
|
|
356
403
|
readOnly?: boolean;
|
|
357
404
|
/** Accessible label / hint for the editor (surfaced via aria-label). */
|
|
358
405
|
placeholder?: string;
|
|
406
|
+
/**
|
|
407
|
+
* Lazy Automatic Type Acquisition resolver. When provided (TS/JS editors),
|
|
408
|
+
* bare `import`/`require` specifiers in the buffer are parsed (debounced)
|
|
409
|
+
* and each NEW package's `.d.ts` closure is fetched + registered so e.g.
|
|
410
|
+
* `import { debounce } from "lodash"` autocompletes. Injected by the
|
|
411
|
+
* consumer so this component stays plugin-agnostic.
|
|
412
|
+
*/
|
|
413
|
+
acquireTypes?: AcquireTypes;
|
|
414
|
+
/**
|
|
415
|
+
* Install identity (lockfile hash). When it changes, the shared acquired-set
|
|
416
|
+
* resets so types refresh against the new install.
|
|
417
|
+
*/
|
|
418
|
+
acquireResetKey?: string;
|
|
419
|
+
/**
|
|
420
|
+
* The running release's `@checkstack/sdk` editor bundle, as virtual `.d.ts`
|
|
421
|
+
* files to mount (TS/JS editors). Makes `import { defineHealthCheck } from
|
|
422
|
+
* "@checkstack/sdk/healthcheck"` resolve with real, version-matched types.
|
|
423
|
+
* Each file mounts at `file:///<path>` via `addExtraLib`. Fetched live by the
|
|
424
|
+
* consumer (so this component stays network-agnostic + DOM-test-free).
|
|
425
|
+
*/
|
|
426
|
+
sdkTypes?: ReadonlyArray<{ path: string; content: string }>;
|
|
427
|
+
/**
|
|
428
|
+
* Release-version reset key for `sdkTypes`. When it changes, the previously
|
|
429
|
+
* mounted SDK libs are reset so the editor never serves stale SDK types after
|
|
430
|
+
* a deployment upgrade.
|
|
431
|
+
*/
|
|
432
|
+
sdkTypesResetKey?: string;
|
|
433
|
+
/**
|
|
434
|
+
* Importable installed package NAMES (TS/JS editors). When provided, the
|
|
435
|
+
* editor suggests these as completions while the cursor is inside an import
|
|
436
|
+
* specifier string (`import {} from "lod"` -> `lodash`) - solving the
|
|
437
|
+
* lazy-ATA catch-22 where no module is registered yet. Must already exclude
|
|
438
|
+
* `@types/*` companions (you import `lodash`, never `@types/lodash`).
|
|
439
|
+
* Injected by the consumer so this component stays plugin-agnostic.
|
|
440
|
+
*/
|
|
441
|
+
importablePackages?: string[];
|
|
442
|
+
/**
|
|
443
|
+
* When `true`, this editor never CLAIMS the one-time global cold init - it
|
|
444
|
+
* always waits for another (visible) editor to bring the monaco-vscode
|
|
445
|
+
* services up, then mounts. Set this for OFFSCREEN/hidden editors (the
|
|
446
|
+
* automation `ScriptServicesBooter`): a hidden editor's init may never
|
|
447
|
+
* complete, so it must not be the sole initializer. Defaults to `false`.
|
|
448
|
+
*/
|
|
449
|
+
deferInit?: boolean;
|
|
359
450
|
};
|
|
360
451
|
|
|
361
452
|
/**
|
|
362
453
|
* Isolated editor used to validate the Typefox/monaco-vscode stack in the
|
|
363
|
-
* browser.
|
|
454
|
+
* browser. Theme follows `useTheme().resolvedTheme` (`vs` in light mode,
|
|
455
|
+
* `vs-dark` in dark mode), no minimap, automatic layout, word-based suggestions
|
|
364
456
|
* disabled. For `typescript`/`javascript` it injects the `context` types +
|
|
365
457
|
* bracket completions; for markup/text languages it offers `{{ }}` template
|
|
366
458
|
* completions.
|
|
@@ -382,19 +474,81 @@ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
|
|
|
382
474
|
: {};
|
|
383
475
|
})();
|
|
384
476
|
|
|
477
|
+
// The monaco-vscode global API config. Hoisted to module scope (it has no
|
|
478
|
+
// per-editor state) so it can drive a single, app-lifetime global init below.
|
|
479
|
+
const vscodeApiConfig: MonacoVscodeApiConfig = {
|
|
480
|
+
// 'classic' is the standalone axis (no extension host); 'extended' is the
|
|
481
|
+
// extension-host axis we deliberately avoid in this migration.
|
|
482
|
+
$type: "classic",
|
|
483
|
+
// Register the missing ILanguageStatusService (see the override above) so
|
|
484
|
+
// focusing a JSON editor doesn't throw "addStatus is not supported".
|
|
485
|
+
serviceOverrides: { ...languageStatusServiceOverride },
|
|
486
|
+
// Plain editor, no workbench views.
|
|
487
|
+
viewsConfig: { $type: "EditorService" },
|
|
488
|
+
monacoWorkerFactory: ensureStandaloneWorkerFactory,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// ─── How the global monaco-vscode init is serialized ────────────────────────
|
|
492
|
+
//
|
|
493
|
+
// `@codingame/monaco-vscode-api`'s global `initialize()` is ONE-SHOT and can
|
|
494
|
+
// never be torn down or re-run. `@typefox/monaco-editor-react` performs that
|
|
495
|
+
// init itself when a `MonacoEditorReactComp` is given a `vscodeApiConfig`, and
|
|
496
|
+
// that is StrictMode-safe FOR A SINGLE EDITOR (upstream tests cover exactly
|
|
497
|
+
// that). The breakage is two editors on one page (e.g. an open script-action
|
|
498
|
+
// editor PLUS the hidden `ScriptServicesBooter`): both wrappers race the init
|
|
499
|
+
// guard (only set inside an async `start()`), both call `initialize()`, the
|
|
500
|
+
// second throws "Services are already initialized", and the global state is
|
|
501
|
+
// corrupted so NO editor starts. StrictMode (dev only) makes the race
|
|
502
|
+
// deterministic via mount -> unmount -> remount; production works because
|
|
503
|
+
// StrictMode is a no-op there.
|
|
504
|
+
//
|
|
505
|
+
// Fix (per the maintainers' single-editor-init guidance): exactly ONE editor
|
|
506
|
+
// claims the cold init and mounts WITH `vscodeApiConfig` (the proven path);
|
|
507
|
+
// every other editor waits for `areVscodeServicesReady()` and only then mounts
|
|
508
|
+
// (it still passes `vscodeApiConfig`, but @typefox no-ops since the services
|
|
509
|
+
// are already initialized). The hidden booter sets `deferInit` so it never
|
|
510
|
+
// claims - the claimer must be a real, visible editor whose init can complete.
|
|
511
|
+
|
|
512
|
+
// Always-available runtime built-in import specifiers (Node + Bun), derived at
|
|
513
|
+
// build time from the bundled stdlib types. These are importable in the script
|
|
514
|
+
// sandbox regardless of the installed-package allowlist (the sandbox is a Bun
|
|
515
|
+
// subprocess; Bun provides Node's builtins + its own `bun:` modules), and their
|
|
516
|
+
// types are already loaded ambiently via the stdlib bundle - so completing one
|
|
517
|
+
// needs no lazy acquisition. The JSON is a plain `string[]`.
|
|
518
|
+
const BUILTIN_MODULE_SPECIFIERS: readonly string[] = builtinModulesJson;
|
|
519
|
+
|
|
520
|
+
// Passed as the wrapper's `onError` so a wrapper failure is surfaced here
|
|
521
|
+
// instead of becoming an uncaught promise rejection (and so @typefox doesn't
|
|
522
|
+
// reset its internal run-queue lock + re-throw).
|
|
523
|
+
const handleEditorError = (error: Error): void => {
|
|
524
|
+
console.error("[CodeEditor] monaco editor error:", error);
|
|
525
|
+
};
|
|
526
|
+
|
|
385
527
|
export const TypefoxEditor = ({
|
|
386
528
|
id,
|
|
387
529
|
value,
|
|
388
530
|
onChange,
|
|
389
531
|
language = "typescript",
|
|
390
532
|
minHeight = 240,
|
|
533
|
+
fillHeight = false,
|
|
391
534
|
typeDefinitions,
|
|
392
535
|
templateProperties,
|
|
393
536
|
shellEnvVars,
|
|
394
537
|
markers,
|
|
395
538
|
readOnly = false,
|
|
396
539
|
placeholder,
|
|
540
|
+
acquireTypes,
|
|
541
|
+
acquireResetKey,
|
|
542
|
+
sdkTypes,
|
|
543
|
+
sdkTypesResetKey,
|
|
544
|
+
importablePackages,
|
|
545
|
+
deferInit = false,
|
|
397
546
|
}: TypefoxEditorProps) => {
|
|
547
|
+
// Follow the app's resolved theme so the editor uses `vs` (light) or
|
|
548
|
+
// `vs-dark` (dark) and updates live when the user toggles the theme.
|
|
549
|
+
const { resolvedTheme } = useTheme();
|
|
550
|
+
const monacoTheme = MONACO_THEME_MAP[resolvedTheme];
|
|
551
|
+
|
|
398
552
|
// `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
|
|
399
553
|
// the handler it calls would otherwise close over a stale `onChange` (bound
|
|
400
554
|
// to the value/sibling-config at mount time). Routing through a ref that we
|
|
@@ -419,8 +573,62 @@ export const TypefoxEditor = ({
|
|
|
419
573
|
// completion providers below register against a ready languages registry.
|
|
420
574
|
const [apiReady, setApiReady] = useState(false);
|
|
421
575
|
|
|
576
|
+
// Cold-init serialization (see the block comment above). Tracks whether the
|
|
577
|
+
// global services are up yet, and whether THIS editor is the designated
|
|
578
|
+
// initializer (the one that mounts first, with `vscodeApiConfig`).
|
|
579
|
+
const [servicesReady, setServicesReady] = useState(() =>
|
|
580
|
+
areVscodeServicesReady(),
|
|
581
|
+
);
|
|
582
|
+
const [isInitializer, setIsInitializer] = useState(false);
|
|
422
583
|
useEffect(() => {
|
|
423
|
-
if (
|
|
584
|
+
if (servicesReady) return;
|
|
585
|
+
return onVscodeServicesReady(() => setServicesReady(true));
|
|
586
|
+
}, [servicesReady]);
|
|
587
|
+
// Decide the initializer role in a layout effect (NOT during render) so it is
|
|
588
|
+
// StrictMode-safe: StrictMode runs setup -> cleanup -> setup, and the cleanup
|
|
589
|
+
// releases the claim, so exactly one editor ends up the stable holder. A
|
|
590
|
+
// `deferInit` editor (the hidden booter) never claims - the initializer must
|
|
591
|
+
// be a real, visible editor whose @typefox init can actually complete.
|
|
592
|
+
useLayoutEffect(() => {
|
|
593
|
+
if (areVscodeServicesReady() || deferInit) return;
|
|
594
|
+
const claimed = claimColdInit();
|
|
595
|
+
setIsInitializer(claimed);
|
|
596
|
+
return () => {
|
|
597
|
+
if (claimed) releaseColdInit();
|
|
598
|
+
};
|
|
599
|
+
}, [deferInit]);
|
|
600
|
+
|
|
601
|
+
// Mount the wrapper when the services are ready (we attach to them) OR when we
|
|
602
|
+
// are the initializer (we bring them up, passing `vscodeApiConfig`). Until
|
|
603
|
+
// then, a sized, non-animated placeholder so the layout doesn't jump.
|
|
604
|
+
const canMountWrapper = servicesReady || isInitializer;
|
|
605
|
+
|
|
606
|
+
// Keep the Monaco global theme and the variable-token decoration color in
|
|
607
|
+
// sync whenever the resolved app theme changes. Monaco's theme is a global
|
|
608
|
+
// imperative setting (not per-editor), so re-deriving `editorAppConfig`
|
|
609
|
+
// alone is not enough after the editor has started - this effect is what
|
|
610
|
+
// makes live toggling work.
|
|
611
|
+
//
|
|
612
|
+
// GATED on `apiReady`: `monaco.editor.setTheme()` resolves a service via
|
|
613
|
+
// `StandaloneServices.get()`, which AUTO-INITIALIZES the monaco-vscode
|
|
614
|
+
// services if they aren't up yet (CodinGame standaloneServices.js:963). If
|
|
615
|
+
// this ran before the wrapper's own init (e.g. on the deferred booter, which
|
|
616
|
+
// mounts but never gets `apiReady`), it would trip CodinGame's
|
|
617
|
+
// `servicesInitialized` flag and make the wrapper's later `initialize()` throw
|
|
618
|
+
// "Services are already initialized". The initial theme is already applied via
|
|
619
|
+
// `editorAppConfig.editorOptions.theme`; this effect only handles live
|
|
620
|
+
// toggling, which is always after the editor (and thus the API) has started.
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (!apiReady) return;
|
|
623
|
+
monacoRuntime.editor.setTheme(monacoTheme);
|
|
624
|
+
ensureVariableTokenStyle({ resolvedTheme });
|
|
625
|
+
}, [apiReady, resolvedTheme, monacoTheme]);
|
|
626
|
+
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
// GATED on `apiReady` for the same reason as the theme effect above:
|
|
629
|
+
// touching `typescriptDefaults` before the wrapper init can auto-initialize
|
|
630
|
+
// the services and collide with the wrapper's `initialize()`.
|
|
631
|
+
if (!apiReady || !isTsLike || typeDefinitions === undefined) {
|
|
424
632
|
return;
|
|
425
633
|
}
|
|
426
634
|
// Inject this editor's ambient `context` types. addExtraLib keys by path
|
|
@@ -436,7 +644,150 @@ export const TypefoxEditor = ({
|
|
|
436
644
|
return () => {
|
|
437
645
|
lib.dispose();
|
|
438
646
|
};
|
|
439
|
-
}, [isTsLike, typeDefinitions, modelId]);
|
|
647
|
+
}, [apiReady, isTsLike, typeDefinitions, modelId]);
|
|
648
|
+
|
|
649
|
+
// Mount the @checkstack/sdk editor bundle (script helpers + typed client) so
|
|
650
|
+
// `import ... from "@checkstack/sdk/healthcheck"` resolves with real types.
|
|
651
|
+
// Shared/module-scoped + reset-on-version (mountSdkTypes); the fetch lives in
|
|
652
|
+
// the consumer so this component is network-agnostic + DOM-test-free.
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
if (!apiReady || !isTsLike || sdkTypes === undefined) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
mountSdkTypes({ files: sdkTypes, resetKey: sdkTypesResetKey });
|
|
658
|
+
}, [apiReady, isTsLike, sdkTypes, sdkTypesResetKey]);
|
|
659
|
+
|
|
660
|
+
// Lazy Automatic Type Acquisition (ATA). For TS/JS editors with an injected
|
|
661
|
+
// `acquireTypes` resolver, parse the buffer's bare import/require specifiers
|
|
662
|
+
// (debounced) and fetch + register each NEW package's `.d.ts` closure, so
|
|
663
|
+
// `import { x } from "pkg"` autocompletes. The acquired-set is module-scoped
|
|
664
|
+
// and shared across editors (the declarations are install-global); the pure
|
|
665
|
+
// parse/plan steps are unit-tested in importSpecifiers.test.ts. Re-running
|
|
666
|
+
// on a new `acquireTypes`/`acquireResetKey` identity is cheap and safe — the
|
|
667
|
+
// module-scoped acquired-set dedupes, so it never double-fetches.
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
if (!apiReady || !isTsLike || acquireTypes === undefined) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const model = findModelById(modelId);
|
|
673
|
+
if (!model) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const acquire = (source: string): void => {
|
|
678
|
+
void runTypeAcquisition({
|
|
679
|
+
source,
|
|
680
|
+
acquireTypes,
|
|
681
|
+
resetKey: acquireResetKey,
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Run once for the initial content so existing imports resolve on open.
|
|
686
|
+
acquire(model.getValue());
|
|
687
|
+
|
|
688
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
689
|
+
const subscription = model.onDidChangeContent(() => {
|
|
690
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
691
|
+
timer = setTimeout(() => {
|
|
692
|
+
acquire(model.getValue());
|
|
693
|
+
}, 400);
|
|
694
|
+
});
|
|
695
|
+
return () => {
|
|
696
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
697
|
+
subscription.dispose();
|
|
698
|
+
};
|
|
699
|
+
}, [apiReady, isTsLike, acquireTypes, acquireResetKey, modelId]);
|
|
700
|
+
|
|
701
|
+
// Controlled-value sync. `value` is only seeded into the model at mount
|
|
702
|
+
// (codeResources), so the editor is otherwise uncontrolled. Apply external
|
|
703
|
+
// `value` changes — a sibling editor's edits (the inline ↔ popout pair share
|
|
704
|
+
// one controlled `value`), a YAML→Visual reset, a loaded definition — to this
|
|
705
|
+
// model. Guarded by an equality check: the user's own edit round-trips
|
|
706
|
+
// `value === model.getValue()`, so the actively-edited editor is a no-op and
|
|
707
|
+
// there is no feedback loop; only a background editor whose `value` prop
|
|
708
|
+
// diverged gets updated.
|
|
709
|
+
useEffect(() => {
|
|
710
|
+
if (!apiReady) return;
|
|
711
|
+
const model = findModelById(modelId);
|
|
712
|
+
if (!model || model.getValue() === value) return;
|
|
713
|
+
model.setValue(value);
|
|
714
|
+
}, [apiReady, modelId, value]);
|
|
715
|
+
|
|
716
|
+
// Import-specifier name completions. Lazy ATA only registers a package's
|
|
717
|
+
// types AFTER its name is in the buffer, so while the user is still TYPING
|
|
718
|
+
// the specifier (`import {} from "lod"`) no module exists yet and the TS
|
|
719
|
+
// worker offers nothing. This provider fills that gap: when the cursor is
|
|
720
|
+
// inside an import/require string (detected by the unit-tested
|
|
721
|
+
// `importSpecifierCompletionContext`), it suggests:
|
|
722
|
+
// - the always-available runtime built-ins (`node:fs`, `bun`, ...), so
|
|
723
|
+
// they appear even with an empty allowlist; AND
|
|
724
|
+
// - the injected installed-package names (already `@types/*`-free).
|
|
725
|
+
// Selecting a built-in inserts an already-typed module; selecting an
|
|
726
|
+
// installed package triggers the ATA loop to load its closure. The list is
|
|
727
|
+
// merged + deduped + sorted by `mergeImportCompletionEntries` (a unit-tested
|
|
728
|
+
// pure helper). Built-ins read as "Node.js" / "Bun built-in" via `detail`.
|
|
729
|
+
// Scoped to THIS model; only the import-string position triggers it, so it
|
|
730
|
+
// never pollutes normal completions. Always registered (built-ins are
|
|
731
|
+
// always available), independent of the allowlist.
|
|
732
|
+
useEffect(() => {
|
|
733
|
+
if (!apiReady || !isTsLike) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const entries = mergeImportCompletionEntries({
|
|
737
|
+
builtins: BUILTIN_MODULE_SPECIFIERS,
|
|
738
|
+
installedPackages: importablePackages ?? [],
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const provideCompletionItems = (
|
|
742
|
+
model: monaco.editor.ITextModel,
|
|
743
|
+
position: monaco.Position,
|
|
744
|
+
): monaco.languages.CompletionList => {
|
|
745
|
+
if (!model.uri.toString().includes(modelId)) {
|
|
746
|
+
return { suggestions: [] };
|
|
747
|
+
}
|
|
748
|
+
const lineUpToCursor = model
|
|
749
|
+
.getLineContent(position.lineNumber)
|
|
750
|
+
.slice(0, position.column - 1);
|
|
751
|
+
const ctx = importSpecifierCompletionContext(lineUpToCursor);
|
|
752
|
+
if (!ctx) {
|
|
753
|
+
return { suggestions: [] };
|
|
754
|
+
}
|
|
755
|
+
// Replace the whole partial specifier (between the quotes) without
|
|
756
|
+
// touching the quotes themselves.
|
|
757
|
+
const range = {
|
|
758
|
+
startLineNumber: position.lineNumber,
|
|
759
|
+
startColumn: ctx.replaceFromColumn,
|
|
760
|
+
endLineNumber: position.lineNumber,
|
|
761
|
+
endColumn: position.column,
|
|
762
|
+
};
|
|
763
|
+
return {
|
|
764
|
+
suggestions: entries.map((entry) => ({
|
|
765
|
+
label: entry.name,
|
|
766
|
+
kind: monacoRuntime.languages.CompletionItemKind.Module,
|
|
767
|
+
detail: entry.detail,
|
|
768
|
+
insertText: entry.name,
|
|
769
|
+
filterText: entry.name,
|
|
770
|
+
range,
|
|
771
|
+
})),
|
|
772
|
+
// The list is the full known set; let monaco filter by `partial`.
|
|
773
|
+
incomplete: false,
|
|
774
|
+
};
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
778
|
+
monacoRuntime.languages.registerCompletionItemProvider(lang, {
|
|
779
|
+
// Opening quotes start a specifier; `:` advances into a `node:`/`bun:`
|
|
780
|
+
// builtin; `/` advances into a scoped name or subpath.
|
|
781
|
+
triggerCharacters: ['"', "'", "/", ":"],
|
|
782
|
+
provideCompletionItems,
|
|
783
|
+
}),
|
|
784
|
+
);
|
|
785
|
+
return () => {
|
|
786
|
+
for (const disposable of disposables) {
|
|
787
|
+
disposable.dispose();
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}, [apiReady, isTsLike, importablePackages, modelId]);
|
|
440
791
|
|
|
441
792
|
// Type-driven bracket-notation completions. The standalone TS worker omits
|
|
442
793
|
// object members whose keys aren't valid identifiers (artifact ids like
|
|
@@ -485,7 +836,7 @@ export const TypefoxEditor = ({
|
|
|
485
836
|
return {
|
|
486
837
|
suggestions: keys.map((key) => ({
|
|
487
838
|
label: `["${key}"]`,
|
|
488
|
-
kind:
|
|
839
|
+
kind: monacoRuntime.languages.CompletionItemKind.Property,
|
|
489
840
|
detail: objectExpression,
|
|
490
841
|
insertText: `["${key}"]`,
|
|
491
842
|
filterText: key,
|
|
@@ -514,7 +865,7 @@ export const TypefoxEditor = ({
|
|
|
514
865
|
};
|
|
515
866
|
|
|
516
867
|
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
517
|
-
|
|
868
|
+
monacoRuntime.languages.registerCompletionItemProvider(lang, {
|
|
518
869
|
triggerCharacters: ["."],
|
|
519
870
|
provideCompletionItems,
|
|
520
871
|
}),
|
|
@@ -565,7 +916,7 @@ export const TypefoxEditor = ({
|
|
|
565
916
|
)
|
|
566
917
|
.map((prop, index) => ({
|
|
567
918
|
label: `{{${prop.path}}}`,
|
|
568
|
-
kind:
|
|
919
|
+
kind: monacoRuntime.languages.CompletionItemKind.Variable,
|
|
569
920
|
detail: prop.type,
|
|
570
921
|
documentation: prop.description,
|
|
571
922
|
insertText: `{{${prop.path}}}`,
|
|
@@ -584,7 +935,7 @@ export const TypefoxEditor = ({
|
|
|
584
935
|
return { suggestions, incomplete: false };
|
|
585
936
|
};
|
|
586
937
|
|
|
587
|
-
const provider =
|
|
938
|
+
const provider = monacoRuntime.languages.registerCompletionItemProvider(
|
|
588
939
|
languageId,
|
|
589
940
|
{ triggerCharacters: ["{"], provideCompletionItems },
|
|
590
941
|
);
|
|
@@ -686,7 +1037,7 @@ export const TypefoxEditor = ({
|
|
|
686
1037
|
)
|
|
687
1038
|
.map((v, index) => ({
|
|
688
1039
|
label: `$${v.name}`,
|
|
689
|
-
kind:
|
|
1040
|
+
kind: monacoRuntime.languages.CompletionItemKind.Variable,
|
|
690
1041
|
detail: v.example ? `e.g. ${v.example}` : "shell env var",
|
|
691
1042
|
// Full name in the (wrapping) docs panel so long CHECKSTACK_* names
|
|
692
1043
|
// stay legible even when the suggest-list label truncates.
|
|
@@ -707,7 +1058,7 @@ export const TypefoxEditor = ({
|
|
|
707
1058
|
return { suggestions, incomplete: false };
|
|
708
1059
|
};
|
|
709
1060
|
|
|
710
|
-
const provider =
|
|
1061
|
+
const provider = monacoRuntime.languages.registerCompletionItemProvider("shell", {
|
|
711
1062
|
triggerCharacters: ["$", "{"],
|
|
712
1063
|
provideCompletionItems,
|
|
713
1064
|
});
|
|
@@ -723,7 +1074,7 @@ export const TypefoxEditor = ({
|
|
|
723
1074
|
if (!apiReady) {
|
|
724
1075
|
return;
|
|
725
1076
|
}
|
|
726
|
-
const model =
|
|
1077
|
+
const model = monacoRuntime.editor
|
|
727
1078
|
.getModels()
|
|
728
1079
|
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
729
1080
|
if (!model) {
|
|
@@ -733,14 +1084,14 @@ export const TypefoxEditor = ({
|
|
|
733
1084
|
severity: EditorMarker["severity"],
|
|
734
1085
|
): monaco.MarkerSeverity => {
|
|
735
1086
|
if (severity === "warning") {
|
|
736
|
-
return
|
|
1087
|
+
return monacoRuntime.MarkerSeverity.Warning;
|
|
737
1088
|
}
|
|
738
1089
|
if (severity === "info") {
|
|
739
|
-
return
|
|
1090
|
+
return monacoRuntime.MarkerSeverity.Info;
|
|
740
1091
|
}
|
|
741
|
-
return
|
|
1092
|
+
return monacoRuntime.MarkerSeverity.Error;
|
|
742
1093
|
};
|
|
743
|
-
|
|
1094
|
+
monacoRuntime.editor.setModelMarkers(
|
|
744
1095
|
model,
|
|
745
1096
|
"external-validation",
|
|
746
1097
|
(markers ?? []).map((marker) => ({
|
|
@@ -753,7 +1104,7 @@ export const TypefoxEditor = ({
|
|
|
753
1104
|
})),
|
|
754
1105
|
);
|
|
755
1106
|
return () => {
|
|
756
|
-
|
|
1107
|
+
monacoRuntime.editor.setModelMarkers(model, "external-validation", []);
|
|
757
1108
|
};
|
|
758
1109
|
}, [apiReady, markers, modelId]);
|
|
759
1110
|
|
|
@@ -775,7 +1126,7 @@ export const TypefoxEditor = ({
|
|
|
775
1126
|
const owner = "template-validation";
|
|
776
1127
|
const runValidation = (): void => {
|
|
777
1128
|
const diagnostics = validate(model.getValue());
|
|
778
|
-
|
|
1129
|
+
monacoRuntime.editor.setModelMarkers(
|
|
779
1130
|
model,
|
|
780
1131
|
owner,
|
|
781
1132
|
diagnostics.map((diagnostic) => {
|
|
@@ -789,7 +1140,7 @@ export const TypefoxEditor = ({
|
|
|
789
1140
|
endLineNumber: end.lineNumber,
|
|
790
1141
|
endColumn: end.column,
|
|
791
1142
|
message: diagnostic.message,
|
|
792
|
-
severity:
|
|
1143
|
+
severity: monacoRuntime.MarkerSeverity.Error,
|
|
793
1144
|
};
|
|
794
1145
|
}),
|
|
795
1146
|
);
|
|
@@ -800,25 +1151,10 @@ export const TypefoxEditor = ({
|
|
|
800
1151
|
});
|
|
801
1152
|
return () => {
|
|
802
1153
|
subscription.dispose();
|
|
803
|
-
|
|
1154
|
+
monacoRuntime.editor.setModelMarkers(model, owner, []);
|
|
804
1155
|
};
|
|
805
1156
|
}, [apiReady, language, modelId]);
|
|
806
1157
|
|
|
807
|
-
const vscodeApiConfig: MonacoVscodeApiConfig = {
|
|
808
|
-
// 'classic' is the standalone axis (no extension host); 'extended' is the
|
|
809
|
-
// extension-host axis we deliberately avoid in this migration.
|
|
810
|
-
$type: "classic",
|
|
811
|
-
// Register the missing ILanguageStatusService (see the override comment
|
|
812
|
-
// above) so focusing a JSON editor doesn't throw "addStatus is not
|
|
813
|
-
// supported".
|
|
814
|
-
serviceOverrides: { ...languageStatusServiceOverride },
|
|
815
|
-
viewsConfig: {
|
|
816
|
-
// Plain editor, no workbench views.
|
|
817
|
-
$type: "EditorService",
|
|
818
|
-
},
|
|
819
|
-
monacoWorkerFactory: configureStandaloneWorkerFactory,
|
|
820
|
-
};
|
|
821
|
-
|
|
822
1158
|
const editorAppConfig: EditorAppConfig = {
|
|
823
1159
|
// Unique per instance (modelId includes a useId suffix) so multiple editors
|
|
824
1160
|
// sharing the same `id` prop don't collide in the wrapper's app registry.
|
|
@@ -831,10 +1167,12 @@ export const TypefoxEditor = ({
|
|
|
831
1167
|
},
|
|
832
1168
|
},
|
|
833
1169
|
editorOptions: {
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
theme
|
|
1170
|
+
// Derived from useTheme().resolvedTheme: "vs" for light, "vs-dark" for
|
|
1171
|
+
// dark. These are the two built-in classic themes available in the
|
|
1172
|
+
// standalone setup (the VS Code 'Default Dark Modern' theme requires the
|
|
1173
|
+
// extension-host theme-defaults extension, which the standalone setup
|
|
1174
|
+
// omits).
|
|
1175
|
+
theme: monacoTheme,
|
|
838
1176
|
minimap: { enabled: false },
|
|
839
1177
|
automaticLayout: true,
|
|
840
1178
|
// Force completions to come from the TS language service rather than
|
|
@@ -850,18 +1188,49 @@ export const TypefoxEditor = ({
|
|
|
850
1188
|
onChangeRef.current?.(textChanges.modified ?? "");
|
|
851
1189
|
};
|
|
852
1190
|
|
|
1191
|
+
// In `fillHeight` mode the container takes its parent's height so Monaco's
|
|
1192
|
+
// `automaticLayout` resizes to fill a tall flex column (the popout body);
|
|
1193
|
+
// `minHeight` stays as a floor. Otherwise the inline fixed-px behaviour is
|
|
1194
|
+
// preserved exactly.
|
|
1195
|
+
const containerStyle: CSSProperties = fillHeight
|
|
1196
|
+
? { minHeight: `${minHeight}px`, height: "100%" }
|
|
1197
|
+
: { minHeight: `${minHeight}px`, height: `${minHeight}px` };
|
|
1198
|
+
|
|
1199
|
+
// Non-initializer editors wait for the services to be up before mounting (a
|
|
1200
|
+
// sized, non-animated placeholder until then, so the layout doesn't jump).
|
|
1201
|
+
// The initializer mounts immediately and brings the services up.
|
|
1202
|
+
if (!canMountWrapper) {
|
|
1203
|
+
return (
|
|
1204
|
+
<div
|
|
1205
|
+
style={containerStyle}
|
|
1206
|
+
className="w-full rounded-md bg-muted"
|
|
1207
|
+
aria-busy="true"
|
|
1208
|
+
aria-label={placeholder ?? "Loading editor"}
|
|
1209
|
+
/>
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
853
1213
|
return (
|
|
854
1214
|
<MonacoEditorReactComp
|
|
855
|
-
style={
|
|
1215
|
+
style={containerStyle}
|
|
856
1216
|
vscodeApiConfig={vscodeApiConfig}
|
|
857
1217
|
editorAppConfig={editorAppConfig}
|
|
858
1218
|
onTextChanged={handleTextChanged}
|
|
1219
|
+
// Route wrapper errors to our handler rather than letting @typefox reset
|
|
1220
|
+
// its run-queue lock and re-throw as an uncaught rejection (recommended by
|
|
1221
|
+
// the monaco-languageclient maintainers).
|
|
1222
|
+
onError={handleEditorError}
|
|
859
1223
|
onEditorStartDone={() => {
|
|
860
1224
|
// Per-editor ready signal for the completion providers. We use this
|
|
861
1225
|
// (not onVscodeApiInitDone) because the vscode API initialises globally
|
|
862
1226
|
// once, so a second editor never gets its own onVscodeApiInitDone - but
|
|
863
1227
|
// onEditorStartDone fires for each editor instance.
|
|
864
1228
|
setApiReady(true);
|
|
1229
|
+
// The monaco-vscode services are now up. Let the headless script
|
|
1230
|
+
// validator know it may safely use the worker (it must never init the
|
|
1231
|
+
// services itself - that would collide with this wrapper's one-time
|
|
1232
|
+
// init and throw "Services are already initialized").
|
|
1233
|
+
markVscodeServicesReady();
|
|
865
1234
|
}}
|
|
866
1235
|
/>
|
|
867
1236
|
);
|