@checkstack/ui 1.9.0 → 1.11.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/CHANGELOG.md +417 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +2 -2
- package/src/components/ActionCard.tsx +221 -0
- package/src/components/CodeEditor/CodeEditor.tsx +51 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- package/src/components/CodeEditor/index.ts +2 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +109 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/DynamicForm/DynamicForm.tsx +2 -0
- package/src/components/DynamicForm/FormField.tsx +29 -9
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
- package/src/components/DynamicForm/types.ts +11 -0
- package/src/components/ListEmptyState.tsx +51 -0
- package/src/components/QueryErrorState.tsx +64 -0
- package/src/components/ResponsiveTable.tsx +92 -0
- package/src/components/Skeleton.tsx +39 -0
- package/src/components/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +10 -0
- package/src/utils/toastTemplates.test.ts +82 -0
- package/src/utils/toastTemplates.ts +47 -0
- package/stories/ActionCard.stories.tsx +62 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/ListEmptyState.stories.tsx +48 -0
- package/stories/QueryErrorState.stories.tsx +40 -0
- package/stories/ResponsiveTable.stories.tsx +93 -0
- package/stories/Skeleton.stories.tsx +53 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/stories/toastTemplates.stories.tsx +60 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- package/src/components/CodeEditor/monacoWorkers.ts +0 -118
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
// Stage 1 foundation for the monaco-editor -> @typefox/monaco-editor-react
|
|
3
|
+
// migration. This is an ISOLATED test vehicle: it renders a single plain
|
|
4
|
+
// TypeScript editor with completions + diagnostics powered by the STANDALONE
|
|
5
|
+
// (classic) TypeScript language features. It is intentionally NOT wired into
|
|
6
|
+
// the existing `CodeEditor` / `MonacoEditor.tsx` and must NOT be re-exported
|
|
7
|
+
// from the package barrel (`src/index.ts`) because the underlying
|
|
8
|
+
// `@codingame/monaco-vscode-*` stack is browser-only and would break any
|
|
9
|
+
// SSR/test path that imports the barrel.
|
|
10
|
+
//
|
|
11
|
+
// API + config shape verified against the INSTALLED package types
|
|
12
|
+
// (@typefox/monaco-editor-react@7.7.0, monaco-languageclient@10.7.0, all
|
|
13
|
+
// @codingame/monaco-vscode-*@25.1.2) and the canonical standalone/classic
|
|
14
|
+
// worker registration in the upstream repo:
|
|
15
|
+
// https://github.com/TypeFox/monaco-languageclient/blob/main/packages/client/test/support/helper-classic.ts
|
|
16
|
+
// https://github.com/TypeFox/monaco-languageclient/blob/main/packages/client/test/worker/workerLoaders.test.ts
|
|
17
|
+
|
|
18
|
+
// Side-effect import registers the classic Monaco language contributions
|
|
19
|
+
// (Monarch grammars for ~80 languages) without pulling in the extension host
|
|
20
|
+
// (no SharedArrayBuffer / COOP / COEP needed).
|
|
21
|
+
import "@codingame/monaco-vscode-standalone-languages";
|
|
22
|
+
// The named imports below ALSO trigger this package's side-effect registration
|
|
23
|
+
// of the standalone TypeScript language features (defaults + ts.worker). We use
|
|
24
|
+
// them to configure the TS/JS language services + inject ambient context types.
|
|
25
|
+
import {
|
|
26
|
+
typescriptDefaults,
|
|
27
|
+
javascriptDefaults,
|
|
28
|
+
ScriptTarget,
|
|
29
|
+
ModuleKind,
|
|
30
|
+
ModuleResolutionKind,
|
|
31
|
+
} from "@codingame/monaco-vscode-standalone-typescript-language-features";
|
|
32
|
+
// Named import also triggers the side-effect registration of the REAL VS Code
|
|
33
|
+
// JSON language service (proper highlighting + completion + folding), replacing
|
|
34
|
+
// the hand-rolled `json-template` Monarch grammar. We turn its built-in
|
|
35
|
+
// (raw-text) validation OFF and validate the template-substituted form instead
|
|
36
|
+
// (see validateJsonTemplate), so templates work in any position - including
|
|
37
|
+
// unquoted ones like a numeric `"timeout": {{x}}`.
|
|
38
|
+
import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-features";
|
|
39
|
+
// Default export is `getServiceOverride()`, returning a service-id -> descriptor
|
|
40
|
+
// map. We register ONLY its `ILanguageStatusService` entry (see
|
|
41
|
+
// `languageStatusServiceOverride` below).
|
|
42
|
+
import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
|
|
43
|
+
|
|
44
|
+
// Worker entry URLs, bundled and resolved by Vite via the `?worker&url`
|
|
45
|
+
// suffix. We import them as URL STRINGS (not Worker constructors) because
|
|
46
|
+
// monaco-languageclient's worker factory consumes `loader().url.toString()`.
|
|
47
|
+
// Vite's STATIC `?worker&url` resolution is required here: a runtime
|
|
48
|
+
// `new URL(specifier, import.meta.url)` would resolve the bare specifier
|
|
49
|
+
// relative to THIS source file (e.g. core/ui/src/components/CodeEditor/...)
|
|
50
|
+
// and 404. The editor worker comes from the monaco-editor drop-in
|
|
51
|
+
// (@codingame/monaco-vscode-editor-api); the TypeScript worker (which also
|
|
52
|
+
// serves JavaScript) from the standalone language-features package.
|
|
53
|
+
import editorWorkerUrl from "@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js?worker&url";
|
|
54
|
+
import tsWorkerUrl from "@codingame/monaco-vscode-standalone-typescript-language-features/worker?worker&url";
|
|
55
|
+
import jsonWorkerUrl from "@codingame/monaco-vscode-standalone-json-language-features/worker?worker&url";
|
|
56
|
+
|
|
57
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
58
|
+
import * as monaco from "@codingame/monaco-vscode-editor-api";
|
|
59
|
+
import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
|
|
60
|
+
import { extractBracketKeyGroups } from "./bracketKeyGroups";
|
|
61
|
+
import { validateJsonTemplate } from "./validateJsonTemplate";
|
|
62
|
+
import { validateYamlTemplate } from "./validateYamlTemplate";
|
|
63
|
+
import { validateXmlTemplate } from "./validateXmlTemplate";
|
|
64
|
+
import type { TemplateDiagnostic } from "./templateValidation";
|
|
65
|
+
import { detectAutoClosedBraces, detectOpenTemplate } from "./templateUtils";
|
|
66
|
+
import {
|
|
67
|
+
buildShellEnvVarInsertText,
|
|
68
|
+
matchShellEnvVarTrigger,
|
|
69
|
+
} from "./shellEnvVarMatcher";
|
|
70
|
+
import type {
|
|
71
|
+
CodeEditorLanguage,
|
|
72
|
+
EditorMarker,
|
|
73
|
+
ShellEnvVar,
|
|
74
|
+
TemplateProperty,
|
|
75
|
+
} from "./types";
|
|
76
|
+
import {
|
|
77
|
+
type EditorAppConfig,
|
|
78
|
+
type TextContents,
|
|
79
|
+
} from "monaco-languageclient/editorApp";
|
|
80
|
+
import { type MonacoVscodeApiConfig } from "monaco-languageclient/vscodeApiWrapper";
|
|
81
|
+
import {
|
|
82
|
+
// `useWorkerFactory` is a plain library registration function, not a React
|
|
83
|
+
// hook. We alias away the `use` prefix so the `react-hooks/rules-of-hooks`
|
|
84
|
+
// lint rule (which keys purely off the identifier name) does not misfire.
|
|
85
|
+
useWorkerFactory as registerWorkerFactory,
|
|
86
|
+
Worker,
|
|
87
|
+
type WorkerFactoryConfig,
|
|
88
|
+
type WorkerLoader,
|
|
89
|
+
} from "monaco-languageclient/workerFactory";
|
|
90
|
+
|
|
91
|
+
// The logger type originates from `@codingame/monaco-vscode-log-service-override`,
|
|
92
|
+
// which is not a direct dependency of this package. We derive it from the
|
|
93
|
+
// `WorkerFactoryConfig` we already import so we never reach for a transitive
|
|
94
|
+
// specifier (and never need an `any`).
|
|
95
|
+
type WorkerFactoryLogger = WorkerFactoryConfig["logger"];
|
|
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
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Registers the worker loaders required for the standalone (classic) Monaco
|
|
108
|
+
* setup. We only need the generic editor worker plus the TypeScript worker
|
|
109
|
+
* (which also serves JavaScript). Mirrors the upstream `defineClassicWorkers`
|
|
110
|
+
* helper referenced above. The underlying `useWorkerFactory` export is a
|
|
111
|
+
* plain library registration function (not a React hook) despite its name;
|
|
112
|
+
* it is called from module scope here, never from a component render.
|
|
113
|
+
*/
|
|
114
|
+
const configureStandaloneWorkerFactory = (
|
|
115
|
+
logger?: WorkerFactoryLogger,
|
|
116
|
+
): void => {
|
|
117
|
+
registerWorkerFactory({
|
|
118
|
+
workerLoaders: {
|
|
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,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Configure the standalone TS + JS language services ONCE at module load.
|
|
147
|
+
* `typescriptDefaults` / `javascriptDefaults` are singletons, so doing this at
|
|
148
|
+
* module scope (not per-mount) guarantees the first editor to mount cannot
|
|
149
|
+
* start the service with stale defaults - the timing race the legacy monaco
|
|
150
|
+
* editor hit.
|
|
151
|
+
*/
|
|
152
|
+
const configureTypeScriptDefaults = (): void => {
|
|
153
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
154
|
+
defaults.setCompilerOptions({ ...BASE_COMPILER_OPTIONS });
|
|
155
|
+
// 1108: a top-level `return` is valid because the runtime wraps scripts in
|
|
156
|
+
// an async IIFE (same suppression as the legacy editor).
|
|
157
|
+
defaults.setDiagnosticsOptions({ diagnosticCodesToIgnore: [1108] });
|
|
158
|
+
// Push models to the worker eagerly so diagnostics/completions are ready on
|
|
159
|
+
// the first keystroke.
|
|
160
|
+
defaults.setEagerModelSync(true);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
configureTypeScriptDefaults();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Lazy-load the bundled `@types/node` + `bun-types` declarations into the
|
|
168
|
+
* standalone TS service so script editors have `console`, `fetch`, `process`,
|
|
169
|
+
* `Bun`, etc. typed (parity with the legacy editor). The ~3 MB bundle is
|
|
170
|
+
* code-split into its own chunk and fetched once. Ported from the legacy
|
|
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.
|
|
173
|
+
*/
|
|
174
|
+
let stdlibLoadStarted = false;
|
|
175
|
+
const ensureStandaloneStdlib = async (): Promise<void> => {
|
|
176
|
+
if (stdlibLoadStarted) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
stdlibLoadStarted = true;
|
|
180
|
+
const stdlibModule = await import("./generated/stdlib-types.json");
|
|
181
|
+
const bundle = stdlibModule.default;
|
|
182
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
183
|
+
for (const [path, content] of Object.entries(bundle)) {
|
|
184
|
+
defaults.addExtraLib(content, `file:///${path}`);
|
|
185
|
+
}
|
|
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
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
void ensureStandaloneStdlib();
|
|
196
|
+
|
|
197
|
+
// Turn OFF the JSON service's built-in validation. The editor content is a
|
|
198
|
+
// template that renders to JSON, so we validate the template-substituted form
|
|
199
|
+
// ourselves (see the json validation effect + validateJsonTemplate) to tolerate
|
|
200
|
+
// `{{ }}` in any position. Highlighting + completion from the service stay on.
|
|
201
|
+
jsonDefaults.setDiagnosticsOptions({ validate: false });
|
|
202
|
+
|
|
203
|
+
// Monaco language id per editor language. Matches the ids registered by
|
|
204
|
+
// @codingame/monaco-vscode-standalone-languages (verified: shell is "shell").
|
|
205
|
+
// JSON editors with templates use the custom `json-template` language (below).
|
|
206
|
+
const MONACO_LANGUAGE_ID: Record<CodeEditorLanguage, string> = {
|
|
207
|
+
typescript: "typescript",
|
|
208
|
+
javascript: "javascript",
|
|
209
|
+
json: "json",
|
|
210
|
+
yaml: "yaml",
|
|
211
|
+
xml: "xml",
|
|
212
|
+
markdown: "markdown",
|
|
213
|
+
shell: "shell",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// File extension for the model uri, so the language service / grammar keys off
|
|
217
|
+
// a sensible filename.
|
|
218
|
+
const LANGUAGE_FILE_EXT: Record<CodeEditorLanguage, string> = {
|
|
219
|
+
typescript: "ts",
|
|
220
|
+
javascript: "js",
|
|
221
|
+
json: "json",
|
|
222
|
+
yaml: "yaml",
|
|
223
|
+
xml: "xml",
|
|
224
|
+
markdown: "md",
|
|
225
|
+
shell: "sh",
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const isTsLikeLanguage = (language: CodeEditorLanguage): boolean =>
|
|
229
|
+
language === "typescript" || language === "javascript";
|
|
230
|
+
|
|
231
|
+
// Per-language template-aware validators (markdown has none - no structure to
|
|
232
|
+
// validate). Each validates the template-substituted form so `{{ }}` is
|
|
233
|
+
// tolerated anywhere; see templateValidation.ts.
|
|
234
|
+
const TEMPLATE_VALIDATORS: Partial<
|
|
235
|
+
Record<CodeEditorLanguage, (text: string) => TemplateDiagnostic[]>
|
|
236
|
+
> = {
|
|
237
|
+
json: validateJsonTemplate,
|
|
238
|
+
yaml: validateYamlTemplate,
|
|
239
|
+
xml: validateXmlTemplate,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Variable-like tokens (`{{ template }}` expressions, shell `$env` refs) are
|
|
243
|
+
// highlighted via inline decorations rather than per-language grammars: this
|
|
244
|
+
// works for any language (yaml / xml / markdown have no template grammar; shell
|
|
245
|
+
// doesn't color `$VAR` inside strings) and keeps the color consistent. #9cdcfe
|
|
246
|
+
// is the vs-dark `variable` token color, matching json-template's grammar. The
|
|
247
|
+
// CSS class is injected once.
|
|
248
|
+
const VARIABLE_TOKEN_CLASS = "checkstack-editor-variable";
|
|
249
|
+
let variableTokenStyleInjected = false;
|
|
250
|
+
const ensureVariableTokenStyle = (): void => {
|
|
251
|
+
if (variableTokenStyleInjected) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
variableTokenStyleInjected = true;
|
|
255
|
+
const style = document.createElement("style");
|
|
256
|
+
// `!important` so the decoration color always wins over the underlying token
|
|
257
|
+
// color (.mtkN), making `{{ }}` look identical inside and outside strings.
|
|
258
|
+
style.textContent = `.${VARIABLE_TOKEN_CLASS}{color:#9cdcfe !important;}`;
|
|
259
|
+
document.head.append(style);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Apply (and keep in sync) inline decorations for every match of `pattern` in
|
|
263
|
+
// the model. Returns a disposable that removes the decorations + listener.
|
|
264
|
+
// `pattern` must be a global (/g) regex; its lastIndex is reset per pass.
|
|
265
|
+
const installRegexDecorations = ({
|
|
266
|
+
model,
|
|
267
|
+
pattern,
|
|
268
|
+
className,
|
|
269
|
+
}: {
|
|
270
|
+
model: monaco.editor.ITextModel;
|
|
271
|
+
pattern: RegExp;
|
|
272
|
+
className: string;
|
|
273
|
+
}): monaco.IDisposable => {
|
|
274
|
+
ensureVariableTokenStyle();
|
|
275
|
+
const compute = (): monaco.editor.IModelDeltaDecoration[] => {
|
|
276
|
+
const text = model.getValue();
|
|
277
|
+
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
|
278
|
+
pattern.lastIndex = 0;
|
|
279
|
+
let match = pattern.exec(text);
|
|
280
|
+
while (match !== null) {
|
|
281
|
+
const start = model.getPositionAt(match.index);
|
|
282
|
+
const end = model.getPositionAt(match.index + match[0].length);
|
|
283
|
+
decorations.push({
|
|
284
|
+
range: new monaco.Range(
|
|
285
|
+
start.lineNumber,
|
|
286
|
+
start.column,
|
|
287
|
+
end.lineNumber,
|
|
288
|
+
end.column,
|
|
289
|
+
),
|
|
290
|
+
options: { inlineClassName: className },
|
|
291
|
+
});
|
|
292
|
+
// Guard against a zero-length match looping forever.
|
|
293
|
+
if (pattern.lastIndex === match.index) {
|
|
294
|
+
pattern.lastIndex += 1;
|
|
295
|
+
}
|
|
296
|
+
match = pattern.exec(text);
|
|
297
|
+
}
|
|
298
|
+
return decorations;
|
|
299
|
+
};
|
|
300
|
+
let decorationIds = model.deltaDecorations([], compute());
|
|
301
|
+
const subscription = model.onDidChangeContent(() => {
|
|
302
|
+
decorationIds = model.deltaDecorations(decorationIds, compute());
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
dispose: () => {
|
|
306
|
+
subscription.dispose();
|
|
307
|
+
model.deltaDecorations(decorationIds, []);
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const findModelById = (
|
|
313
|
+
modelId: string,
|
|
314
|
+
): monaco.editor.ITextModel | undefined =>
|
|
315
|
+
monaco.editor
|
|
316
|
+
.getModels()
|
|
317
|
+
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
318
|
+
|
|
319
|
+
export type TypefoxEditorProps = {
|
|
320
|
+
/** Stable identity for the underlying editor app + model uri. */
|
|
321
|
+
id: string;
|
|
322
|
+
/** Initial source rendered in the editor. */
|
|
323
|
+
value: string;
|
|
324
|
+
/** Notified with the latest editor text whenever it changes. */
|
|
325
|
+
onChange?: (value: string) => void;
|
|
326
|
+
/** Editor language. Defaults to `typescript`. */
|
|
327
|
+
language?: CodeEditorLanguage;
|
|
328
|
+
/** Minimum editor height in pixels. Defaults to 240. */
|
|
329
|
+
minHeight?: number;
|
|
330
|
+
/**
|
|
331
|
+
* Generated ambient type definitions (the `context.d.ts`) injected as a TS
|
|
332
|
+
* extra-lib so `context.*` resolves with real fields. Wired up once per
|
|
333
|
+
* editor at mount, keyed by a unique path - no addExtraLib race.
|
|
334
|
+
* Only used for `typescript` / `javascript` editors.
|
|
335
|
+
*/
|
|
336
|
+
typeDefinitions?: string;
|
|
337
|
+
/**
|
|
338
|
+
* Template properties for non-script editors. When provided, typing `{{`
|
|
339
|
+
* autocompletes the available `{{ path }}` references. Only used for
|
|
340
|
+
* markup/text editors (json / yaml / xml / markdown).
|
|
341
|
+
*/
|
|
342
|
+
templateProperties?: TemplateProperty[];
|
|
343
|
+
/**
|
|
344
|
+
* Environment-variable hints for `shell` editors. When provided, typing `$`
|
|
345
|
+
* or `${` autocompletes the variable names. Only used when `language` is
|
|
346
|
+
* `shell`.
|
|
347
|
+
*/
|
|
348
|
+
shellEnvVars?: ShellEnvVar[];
|
|
349
|
+
/**
|
|
350
|
+
* Externally-computed diagnostics rendered as inline squiggles under a
|
|
351
|
+
* dedicated marker owner (so they coexist with monaco's own markers).
|
|
352
|
+
* Positions are 1-based (monaco convention) - e.g. YAML definition validation.
|
|
353
|
+
*/
|
|
354
|
+
markers?: EditorMarker[];
|
|
355
|
+
/** Render the editor read-only. */
|
|
356
|
+
readOnly?: boolean;
|
|
357
|
+
/** Accessible label / hint for the editor (surfaced via aria-label). */
|
|
358
|
+
placeholder?: string;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Isolated editor used to validate the Typefox/monaco-vscode stack in the
|
|
363
|
+
* browser. Dark theme, no minimap, automatic layout, word-based suggestions
|
|
364
|
+
* disabled. For `typescript`/`javascript` it injects the `context` types +
|
|
365
|
+
* bracket completions; for markup/text languages it offers `{{ }}` template
|
|
366
|
+
* completions.
|
|
367
|
+
*/
|
|
368
|
+
// The standalone ("classic") service set omits `ILanguageStatusService`, but
|
|
369
|
+
// the JSON language features register a language-status indicator (the active
|
|
370
|
+
// formatter) on editor focus and throw "LanguageStatusService.addStatus is not
|
|
371
|
+
// supported" without it. We register ONLY that service: the full languages
|
|
372
|
+
// override would also swap `ILanguageService` for the workbench impl and pull
|
|
373
|
+
// in the files-service override, both of which conflict with the standalone
|
|
374
|
+
// language setup. The override map is keyed by the service-decorator id
|
|
375
|
+
// (`createDecorator('ILanguageStatusService')`), so we pick that one entry.
|
|
376
|
+
const LANGUAGE_STATUS_SERVICE_ID = "ILanguageStatusService";
|
|
377
|
+
const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
|
|
378
|
+
(() => {
|
|
379
|
+
const all = getLanguagesServiceOverride();
|
|
380
|
+
return LANGUAGE_STATUS_SERVICE_ID in all
|
|
381
|
+
? { [LANGUAGE_STATUS_SERVICE_ID]: all[LANGUAGE_STATUS_SERVICE_ID] }
|
|
382
|
+
: {};
|
|
383
|
+
})();
|
|
384
|
+
|
|
385
|
+
export const TypefoxEditor = ({
|
|
386
|
+
id,
|
|
387
|
+
value,
|
|
388
|
+
onChange,
|
|
389
|
+
language = "typescript",
|
|
390
|
+
minHeight = 240,
|
|
391
|
+
typeDefinitions,
|
|
392
|
+
templateProperties,
|
|
393
|
+
shellEnvVars,
|
|
394
|
+
markers,
|
|
395
|
+
readOnly = false,
|
|
396
|
+
placeholder,
|
|
397
|
+
}: TypefoxEditorProps) => {
|
|
398
|
+
// `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
|
|
399
|
+
// the handler it calls would otherwise close over a stale `onChange` (bound
|
|
400
|
+
// to the value/sibling-config at mount time). Routing through a ref that we
|
|
401
|
+
// keep current on every render means content changes always invoke the
|
|
402
|
+
// latest `onChange` — without this, editing one DynamicForm field reverts
|
|
403
|
+
// sibling fields (e.g. a shell action's `env`) to their mount-time values.
|
|
404
|
+
const onChangeRef = useRef(onChange);
|
|
405
|
+
onChangeRef.current = onChange;
|
|
406
|
+
|
|
407
|
+
// Unique-per-instance id so multiple editors never share a model or clobber
|
|
408
|
+
// each other's extra-lib.
|
|
409
|
+
const reactId = useId();
|
|
410
|
+
const modelId = `${id}-${reactId.replaceAll(":", "")}`;
|
|
411
|
+
const modelUri = `/workspace/${modelId}.${LANGUAGE_FILE_EXT[language]}`;
|
|
412
|
+
|
|
413
|
+
const isTsLike = isTsLikeLanguage(language);
|
|
414
|
+
const hasTemplates =
|
|
415
|
+
templateProperties !== undefined && templateProperties.length > 0;
|
|
416
|
+
const languageId = MONACO_LANGUAGE_ID[language];
|
|
417
|
+
|
|
418
|
+
// Set once the wrapper has initialised the VS Code services, so the
|
|
419
|
+
// completion providers below register against a ready languages registry.
|
|
420
|
+
const [apiReady, setApiReady] = useState(false);
|
|
421
|
+
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (!isTsLike || typeDefinitions === undefined) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Inject this editor's ambient `context` types. addExtraLib keys by path
|
|
427
|
+
// and re-syncs the worker on change, so the types are reliably picked up
|
|
428
|
+
// (no race with model load). NOTE: extra-libs are ambient/global to the TS
|
|
429
|
+
// service, so two TS editors with *different* `context` shapes mounted at
|
|
430
|
+
// once would collide on the `context` identifier - per-editor isolation is
|
|
431
|
+
// a Stage 6 concern, irrelevant to the single-editor test vehicle here.
|
|
432
|
+
const lib = typescriptDefaults.addExtraLib(
|
|
433
|
+
typeDefinitions,
|
|
434
|
+
`file:///context-${modelId}.d.ts`,
|
|
435
|
+
);
|
|
436
|
+
return () => {
|
|
437
|
+
lib.dispose();
|
|
438
|
+
};
|
|
439
|
+
}, [isTsLike, typeDefinitions, modelId]);
|
|
440
|
+
|
|
441
|
+
// Type-driven bracket-notation completions. The standalone TS worker omits
|
|
442
|
+
// object members whose keys aren't valid identifiers (artifact ids like
|
|
443
|
+
// `integration-jira.issue`), and the built-in SuggestAdapter can't be
|
|
444
|
+
// overridden to insert them, so we register our own provider: typing
|
|
445
|
+
// `<objectExpression>.` lists the keys and accepting one rewrites the dot to
|
|
446
|
+
// `["key"]` (mirrors VS Code's `obj."a-b"` -> `obj["a-b"]`). The groups are
|
|
447
|
+
// derived from the injected `context.d.ts` itself, so no separate prop is
|
|
448
|
+
// threaded. Scoped to THIS editor's model so multiple editors don't cross-feed.
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!apiReady || !isTsLike || typeDefinitions === undefined) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const groups = extractBracketKeyGroups({ typeDefinitions });
|
|
454
|
+
if (groups.length === 0) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const escapeRegExp = (input: string): string =>
|
|
459
|
+
input.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
460
|
+
|
|
461
|
+
const provideCompletionItems = (
|
|
462
|
+
model: monaco.editor.ITextModel,
|
|
463
|
+
position: monaco.Position,
|
|
464
|
+
): monaco.languages.CompletionList => {
|
|
465
|
+
if (!model.uri.toString().includes(modelId)) {
|
|
466
|
+
return { suggestions: [] };
|
|
467
|
+
}
|
|
468
|
+
const textBefore = model
|
|
469
|
+
.getLineContent(position.lineNumber)
|
|
470
|
+
.slice(0, position.column - 1);
|
|
471
|
+
|
|
472
|
+
for (const { objectExpression, keys } of groups) {
|
|
473
|
+
// Match `<objectExpression>.<query>` at the cursor, ensuring the
|
|
474
|
+
// expression isn't the tail of a longer identifier.
|
|
475
|
+
const match = new RegExp(
|
|
476
|
+
String.raw`(?:^|[^\w$.])${escapeRegExp(objectExpression)}\.([\w$]*)$`,
|
|
477
|
+
).exec(textBefore);
|
|
478
|
+
if (!match) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const query = match[1] ?? "";
|
|
482
|
+
const queryStartColumn = position.column - query.length;
|
|
483
|
+
const dotColumn = queryStartColumn - 1;
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
suggestions: keys.map((key) => ({
|
|
487
|
+
label: `["${key}"]`,
|
|
488
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
489
|
+
detail: objectExpression,
|
|
490
|
+
insertText: `["${key}"]`,
|
|
491
|
+
filterText: key,
|
|
492
|
+
range: {
|
|
493
|
+
startLineNumber: position.lineNumber,
|
|
494
|
+
startColumn: queryStartColumn,
|
|
495
|
+
endLineNumber: position.lineNumber,
|
|
496
|
+
endColumn: position.column,
|
|
497
|
+
},
|
|
498
|
+
// Delete the triggering `.` so `obj.` becomes `obj["key"]`.
|
|
499
|
+
additionalTextEdits: [
|
|
500
|
+
{
|
|
501
|
+
range: {
|
|
502
|
+
startLineNumber: position.lineNumber,
|
|
503
|
+
startColumn: dotColumn,
|
|
504
|
+
endLineNumber: position.lineNumber,
|
|
505
|
+
endColumn: dotColumn + 1,
|
|
506
|
+
},
|
|
507
|
+
text: "",
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
})),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return { suggestions: [] };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
517
|
+
monaco.languages.registerCompletionItemProvider(lang, {
|
|
518
|
+
triggerCharacters: ["."],
|
|
519
|
+
provideCompletionItems,
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
return () => {
|
|
523
|
+
for (const disposable of disposables) {
|
|
524
|
+
disposable.dispose();
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}, [apiReady, isTsLike, typeDefinitions, modelId]);
|
|
528
|
+
|
|
529
|
+
// Template `{{ }}` completion for markup/text editors (json / yaml / xml /
|
|
530
|
+
// markdown). Typing `{{` lists the available `{{ path }}` references; ported
|
|
531
|
+
// from the legacy MonacoEditor template provider (uses the same tested
|
|
532
|
+
// `detectOpenTemplate` / `detectAutoClosedBraces` helpers). Registered for
|
|
533
|
+
// THIS editor's resolved language id and scoped to its model.
|
|
534
|
+
useEffect(() => {
|
|
535
|
+
if (!apiReady || isTsLike || !hasTemplates) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const properties = templateProperties ?? [];
|
|
539
|
+
|
|
540
|
+
const provideCompletionItems = (
|
|
541
|
+
model: monaco.editor.ITextModel,
|
|
542
|
+
position: monaco.Position,
|
|
543
|
+
): monaco.languages.CompletionList => {
|
|
544
|
+
if (!model.uri.toString().includes(modelId)) {
|
|
545
|
+
return { suggestions: [] };
|
|
546
|
+
}
|
|
547
|
+
const content = model.getValue();
|
|
548
|
+
const cursorOffset = model.getOffsetAt(position);
|
|
549
|
+
|
|
550
|
+
const openTemplate = detectOpenTemplate({ content, cursorOffset });
|
|
551
|
+
if (!openTemplate.isInTemplate) {
|
|
552
|
+
return { suggestions: [] };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const query = openTemplate.query.toLowerCase();
|
|
556
|
+
const startColumn = openTemplate.startColumn;
|
|
557
|
+
// Monaco may have auto-closed with `}}` after the cursor; extend the
|
|
558
|
+
// replaced range over it so we don't leave dangling braces.
|
|
559
|
+
const endColumn =
|
|
560
|
+
position.column + detectAutoClosedBraces({ content, cursorOffset });
|
|
561
|
+
|
|
562
|
+
const suggestions = properties
|
|
563
|
+
.filter(
|
|
564
|
+
(prop) => query === "" || prop.path.toLowerCase().includes(query),
|
|
565
|
+
)
|
|
566
|
+
.map((prop, index) => ({
|
|
567
|
+
label: `{{${prop.path}}}`,
|
|
568
|
+
kind: monaco.languages.CompletionItemKind.Variable,
|
|
569
|
+
detail: prop.type,
|
|
570
|
+
documentation: prop.description,
|
|
571
|
+
insertText: `{{${prop.path}}}`,
|
|
572
|
+
// Leading space sorts these above the editor's own suggestions.
|
|
573
|
+
sortText: ` ${String(index).padStart(4, "0")}`,
|
|
574
|
+
filterText: `{{${query}${prop.path}`,
|
|
575
|
+
preselect: index === 0,
|
|
576
|
+
range: {
|
|
577
|
+
startLineNumber: position.lineNumber,
|
|
578
|
+
startColumn,
|
|
579
|
+
endLineNumber: position.lineNumber,
|
|
580
|
+
endColumn,
|
|
581
|
+
},
|
|
582
|
+
}));
|
|
583
|
+
|
|
584
|
+
return { suggestions, incomplete: false };
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const provider = monaco.languages.registerCompletionItemProvider(
|
|
588
|
+
languageId,
|
|
589
|
+
{ triggerCharacters: ["{"], provideCompletionItems },
|
|
590
|
+
);
|
|
591
|
+
return () => {
|
|
592
|
+
provider.dispose();
|
|
593
|
+
};
|
|
594
|
+
}, [
|
|
595
|
+
apiReady,
|
|
596
|
+
isTsLike,
|
|
597
|
+
hasTemplates,
|
|
598
|
+
templateProperties,
|
|
599
|
+
languageId,
|
|
600
|
+
modelId,
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
// Highlight `{{ ... }}` template expressions (template editors). The
|
|
604
|
+
// `[^{}]*` body (not `[^}]*`) stops an unclosed `{{` from swallowing text up
|
|
605
|
+
// to a later `}}`.
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
if (!apiReady || isTsLike || !hasTemplates) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const model = findModelById(modelId);
|
|
611
|
+
if (!model) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const handle = installRegexDecorations({
|
|
615
|
+
model,
|
|
616
|
+
pattern: /\{\{[^{}]*\}\}/g,
|
|
617
|
+
className: VARIABLE_TOKEN_CLASS,
|
|
618
|
+
});
|
|
619
|
+
return () => {
|
|
620
|
+
handle.dispose();
|
|
621
|
+
};
|
|
622
|
+
}, [apiReady, isTsLike, hasTemplates, modelId]);
|
|
623
|
+
|
|
624
|
+
// Highlight shell variable references (`$NAME` / `${NAME}`) - the shell
|
|
625
|
+
// grammar doesn't color these inside double-quoted strings, where ours live.
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
if (!apiReady || language !== "shell") {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const model = findModelById(modelId);
|
|
631
|
+
if (!model) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const handle = installRegexDecorations({
|
|
635
|
+
model,
|
|
636
|
+
pattern: /\$\{[A-Za-z_]\w*\}|\$[A-Za-z_]\w*/g,
|
|
637
|
+
className: VARIABLE_TOKEN_CLASS,
|
|
638
|
+
});
|
|
639
|
+
return () => {
|
|
640
|
+
handle.dispose();
|
|
641
|
+
};
|
|
642
|
+
}, [apiReady, language, modelId]);
|
|
643
|
+
|
|
644
|
+
// Shell `$env` completion. For `shell` editors, typing `$` or `${` suggests
|
|
645
|
+
// the provided variable names (and brace-closes `${name}` correctly). Ported
|
|
646
|
+
// from the legacy MonacoEditor shell provider; uses the tested
|
|
647
|
+
// `matchShellEnvVarTrigger` / `buildShellEnvVarInsertText` helpers.
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
if (!apiReady || language !== "shell") {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (shellEnvVars === undefined || shellEnvVars.length === 0) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const envVars = shellEnvVars;
|
|
656
|
+
|
|
657
|
+
const provideCompletionItems = (
|
|
658
|
+
model: monaco.editor.ITextModel,
|
|
659
|
+
position: monaco.Position,
|
|
660
|
+
): monaco.languages.CompletionList => {
|
|
661
|
+
if (!model.uri.toString().includes(modelId)) {
|
|
662
|
+
return { suggestions: [] };
|
|
663
|
+
}
|
|
664
|
+
const lineText = model.getLineContent(position.lineNumber);
|
|
665
|
+
const textBefore = lineText.slice(0, position.column - 1);
|
|
666
|
+
const match = matchShellEnvVarTrigger(textBefore);
|
|
667
|
+
if (!match) {
|
|
668
|
+
return { suggestions: [] };
|
|
669
|
+
}
|
|
670
|
+
const startColumn = position.column - match.prefixLength;
|
|
671
|
+
// `{` auto-closes to `}` in shell, so a braced `${` leaves a `}` right
|
|
672
|
+
// after the cursor. Extend the replace range over it so an accepted
|
|
673
|
+
// `${NAME}` doesn't leave a stray brace (`${NAME}}`).
|
|
674
|
+
const hasAutoClosedBrace =
|
|
675
|
+
match.form === "braced" && lineText[position.column - 1] === "}";
|
|
676
|
+
const endColumn = hasAutoClosedBrace
|
|
677
|
+
? position.column + 1
|
|
678
|
+
: position.column;
|
|
679
|
+
// filterText must match the text already in the replace range (`${` for
|
|
680
|
+
// braced, `$` for bare) or monaco fuzzy-filters every item out.
|
|
681
|
+
const filterPrefix = match.form === "braced" ? "${" : "$";
|
|
682
|
+
|
|
683
|
+
const suggestions = envVars
|
|
684
|
+
.filter(
|
|
685
|
+
(v) => match.query === "" || v.name.toUpperCase().includes(match.query),
|
|
686
|
+
)
|
|
687
|
+
.map((v, index) => ({
|
|
688
|
+
label: `$${v.name}`,
|
|
689
|
+
kind: monaco.languages.CompletionItemKind.Variable,
|
|
690
|
+
detail: v.example ? `e.g. ${v.example}` : "shell env var",
|
|
691
|
+
// Full name in the (wrapping) docs panel so long CHECKSTACK_* names
|
|
692
|
+
// stay legible even when the suggest-list label truncates.
|
|
693
|
+
documentation: {
|
|
694
|
+
value: [`\`$${v.name}\``, v.description].filter(Boolean).join("\n\n"),
|
|
695
|
+
},
|
|
696
|
+
insertText: buildShellEnvVarInsertText(match, v.name),
|
|
697
|
+
sortText: ` ${String(index).padStart(4, "0")}`,
|
|
698
|
+
filterText: `${filterPrefix}${v.name}`,
|
|
699
|
+
range: {
|
|
700
|
+
startLineNumber: position.lineNumber,
|
|
701
|
+
startColumn,
|
|
702
|
+
endLineNumber: position.lineNumber,
|
|
703
|
+
endColumn,
|
|
704
|
+
},
|
|
705
|
+
}));
|
|
706
|
+
|
|
707
|
+
return { suggestions, incomplete: false };
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const provider = monaco.languages.registerCompletionItemProvider("shell", {
|
|
711
|
+
triggerCharacters: ["$", "{"],
|
|
712
|
+
provideCompletionItems,
|
|
713
|
+
});
|
|
714
|
+
return () => {
|
|
715
|
+
provider.dispose();
|
|
716
|
+
};
|
|
717
|
+
}, [apiReady, language, shellEnvVars, modelId]);
|
|
718
|
+
|
|
719
|
+
// External validation markers (inline squiggles). Applied under a dedicated
|
|
720
|
+
// owner so they coexist with monaco's own language markers. Ported from the
|
|
721
|
+
// legacy editor; used e.g. for YAML definition validation in AutomationEditPage.
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
if (!apiReady) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const model = monaco.editor
|
|
727
|
+
.getModels()
|
|
728
|
+
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
729
|
+
if (!model) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const toSeverity = (
|
|
733
|
+
severity: EditorMarker["severity"],
|
|
734
|
+
): monaco.MarkerSeverity => {
|
|
735
|
+
if (severity === "warning") {
|
|
736
|
+
return monaco.MarkerSeverity.Warning;
|
|
737
|
+
}
|
|
738
|
+
if (severity === "info") {
|
|
739
|
+
return monaco.MarkerSeverity.Info;
|
|
740
|
+
}
|
|
741
|
+
return monaco.MarkerSeverity.Error;
|
|
742
|
+
};
|
|
743
|
+
monaco.editor.setModelMarkers(
|
|
744
|
+
model,
|
|
745
|
+
"external-validation",
|
|
746
|
+
(markers ?? []).map((marker) => ({
|
|
747
|
+
startLineNumber: marker.startLineNumber,
|
|
748
|
+
startColumn: marker.startColumn,
|
|
749
|
+
endLineNumber: marker.endLineNumber,
|
|
750
|
+
endColumn: marker.endColumn,
|
|
751
|
+
message: marker.message,
|
|
752
|
+
severity: toSeverity(marker.severity),
|
|
753
|
+
})),
|
|
754
|
+
);
|
|
755
|
+
return () => {
|
|
756
|
+
monaco.editor.setModelMarkers(model, "external-validation", []);
|
|
757
|
+
};
|
|
758
|
+
}, [apiReady, markers, modelId]);
|
|
759
|
+
|
|
760
|
+
// Template-aware validation for markup languages (json / yaml / xml). The
|
|
761
|
+
// language services' own validation is off (json) or absent (yaml/xml);
|
|
762
|
+
// instead we validate the template-substituted form so `{{ }}` is allowed in
|
|
763
|
+
// any position while real structural errors are still flagged. Recomputed on
|
|
764
|
+
// every edit. Under a dedicated owner so it coexists with the external
|
|
765
|
+
// `markers`.
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
const validate = TEMPLATE_VALIDATORS[language];
|
|
768
|
+
if (!apiReady || !validate) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const model = findModelById(modelId);
|
|
772
|
+
if (!model) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const owner = "template-validation";
|
|
776
|
+
const runValidation = (): void => {
|
|
777
|
+
const diagnostics = validate(model.getValue());
|
|
778
|
+
monaco.editor.setModelMarkers(
|
|
779
|
+
model,
|
|
780
|
+
owner,
|
|
781
|
+
diagnostics.map((diagnostic) => {
|
|
782
|
+
const start = model.getPositionAt(diagnostic.offset);
|
|
783
|
+
const end = model.getPositionAt(
|
|
784
|
+
diagnostic.offset + diagnostic.length,
|
|
785
|
+
);
|
|
786
|
+
return {
|
|
787
|
+
startLineNumber: start.lineNumber,
|
|
788
|
+
startColumn: start.column,
|
|
789
|
+
endLineNumber: end.lineNumber,
|
|
790
|
+
endColumn: end.column,
|
|
791
|
+
message: diagnostic.message,
|
|
792
|
+
severity: monaco.MarkerSeverity.Error,
|
|
793
|
+
};
|
|
794
|
+
}),
|
|
795
|
+
);
|
|
796
|
+
};
|
|
797
|
+
runValidation();
|
|
798
|
+
const subscription = model.onDidChangeContent(() => {
|
|
799
|
+
runValidation();
|
|
800
|
+
});
|
|
801
|
+
return () => {
|
|
802
|
+
subscription.dispose();
|
|
803
|
+
monaco.editor.setModelMarkers(model, owner, []);
|
|
804
|
+
};
|
|
805
|
+
}, [apiReady, language, modelId]);
|
|
806
|
+
|
|
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
|
+
const editorAppConfig: EditorAppConfig = {
|
|
823
|
+
// Unique per instance (modelId includes a useId suffix) so multiple editors
|
|
824
|
+
// sharing the same `id` prop don't collide in the wrapper's app registry.
|
|
825
|
+
id: modelId,
|
|
826
|
+
codeResources: {
|
|
827
|
+
modified: {
|
|
828
|
+
text: value,
|
|
829
|
+
uri: modelUri,
|
|
830
|
+
enforceLanguageId: languageId,
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
editorOptions: {
|
|
834
|
+
// 'vs-dark' is the builtin classic dark theme. (The VS Code
|
|
835
|
+
// 'Default Dark Modern' theme would require the extension-host
|
|
836
|
+
// theme-defaults extension, which the standalone setup omits.)
|
|
837
|
+
theme: "vs-dark",
|
|
838
|
+
minimap: { enabled: false },
|
|
839
|
+
automaticLayout: true,
|
|
840
|
+
// Force completions to come from the TS language service rather than
|
|
841
|
+
// naive word matching.
|
|
842
|
+
wordBasedSuggestions: "off",
|
|
843
|
+
scrollBeyondLastLine: false,
|
|
844
|
+
readOnly,
|
|
845
|
+
ariaLabel: placeholder ?? "Code editor",
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const handleTextChanged = (textChanges: TextContents): void => {
|
|
850
|
+
onChangeRef.current?.(textChanges.modified ?? "");
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
return (
|
|
854
|
+
<MonacoEditorReactComp
|
|
855
|
+
style={{ minHeight: `${minHeight}px`, height: `${minHeight}px` }}
|
|
856
|
+
vscodeApiConfig={vscodeApiConfig}
|
|
857
|
+
editorAppConfig={editorAppConfig}
|
|
858
|
+
onTextChanged={handleTextChanged}
|
|
859
|
+
onEditorStartDone={() => {
|
|
860
|
+
// Per-editor ready signal for the completion providers. We use this
|
|
861
|
+
// (not onVscodeApiInitDone) because the vscode API initialises globally
|
|
862
|
+
// once, so a second editor never gets its own onVscodeApiInitDone - but
|
|
863
|
+
// onEditorStartDone fires for each editor instance.
|
|
864
|
+
setApiReady(true);
|
|
865
|
+
}}
|
|
866
|
+
/>
|
|
867
|
+
);
|
|
868
|
+
};
|