@checkstack/ui 1.11.0 → 1.12.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 +181 -0
- package/package.json +4 -4
- package/scripts/generate-stdlib-types.ts +23 -0
- package/src/components/ActionCard.tsx +96 -8
- package/src/components/CodeEditor/CodeEditor.tsx +95 -14
- package/src/components/CodeEditor/TypefoxEditor.tsx +279 -123
- 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 +24 -0
- package/src/components/CodeEditor/monacoTsService.ts +217 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/types.ts +59 -0
- package/src/components/CodeEditor/validateScripts.ts +132 -0
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +25 -1
- package/src/components/DynamicForm/FormField.tsx +109 -1
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +67 -2
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +6 -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 +72 -1
- package/src/components/DynamicForm/utils.ts +32 -0
- 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/TimeOfDayInput.tsx +116 -0
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/index.ts +4 -0
- package/stories/ActionCard.stories.tsx +60 -0
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/tsconfig.json +1 -0
|
@@ -19,16 +19,15 @@
|
|
|
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
|
-
} from "@codingame/monaco-vscode-standalone-typescript-language-features";
|
|
28
|
+
ensureStandaloneWorkerFactory,
|
|
29
|
+
markVscodeServicesReady,
|
|
30
|
+
} from "./monacoTsService";
|
|
32
31
|
// Named import also triggers the side-effect registration of the REAL VS Code
|
|
33
32
|
// JSON language service (proper highlighting + completion + folding), replacing
|
|
34
33
|
// the hand-rolled `json-template` Monarch grammar. We turn its built-in
|
|
@@ -41,20 +40,8 @@ import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-
|
|
|
41
40
|
// `languageStatusServiceOverride` below).
|
|
42
41
|
import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
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";
|
|
43
|
+
|
|
44
|
+
import { type CSSProperties, useEffect, useId, useRef, useState } from "react";
|
|
58
45
|
import * as monaco from "@codingame/monaco-vscode-editor-api";
|
|
59
46
|
import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
|
|
60
47
|
import { extractBracketKeyGroups } from "./bracketKeyGroups";
|
|
@@ -68,132 +55,115 @@ import {
|
|
|
68
55
|
matchShellEnvVarTrigger,
|
|
69
56
|
} from "./shellEnvVarMatcher";
|
|
70
57
|
import type {
|
|
58
|
+
AcquireTypes,
|
|
71
59
|
CodeEditorLanguage,
|
|
72
60
|
EditorMarker,
|
|
73
61
|
ShellEnvVar,
|
|
74
62
|
TemplateProperty,
|
|
75
63
|
} from "./types";
|
|
64
|
+
import {
|
|
65
|
+
importSpecifierCompletionContext,
|
|
66
|
+
mergeImportCompletionEntries,
|
|
67
|
+
parseBareImportSpecifiers,
|
|
68
|
+
planAcquisitions,
|
|
69
|
+
} from "./importSpecifiers";
|
|
70
|
+
// Authoritative, build-time-derived list of importable runtime built-in
|
|
71
|
+
// specifiers (`node:fs`, bare `fs`, `bun`, `bun:test`, ...). Generated from the
|
|
72
|
+
// SAME bundled `@types/node` + `bun-types` declarations the editor injects (see
|
|
73
|
+
// scripts/generate-stdlib-types.ts -> extractBuiltinModuleSpecifiers), so the
|
|
74
|
+
// import-name completions never drift from the runtime stdlib. Imported as a
|
|
75
|
+
// plain JSON module (tiny: ~115 names); the bulky type bodies stay in the
|
|
76
|
+
// separately code-split stdlib-types.json.
|
|
77
|
+
import builtinModulesJson from "./generated/builtin-modules.json";
|
|
76
78
|
import {
|
|
77
79
|
type EditorAppConfig,
|
|
78
80
|
type TextContents,
|
|
79
81
|
} from "monaco-languageclient/editorApp";
|
|
80
82
|
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" });
|
|
83
|
+
// ─── Lazy Automatic Type Acquisition (ATA) registry ─────────────────────────
|
|
84
|
+
//
|
|
85
|
+
// The TS/JS language services are singletons, so acquired package types are
|
|
86
|
+
// registered ONCE and shared across every editor instance (a package imported
|
|
87
|
+
// in one script editor is then typed in all of them — harmless, since the
|
|
88
|
+
// declarations are the same install). State is module-scoped:
|
|
89
|
+
//
|
|
90
|
+
// - `acquiredFilePaths`: virtual paths already passed to addExtraLib (dedupe
|
|
91
|
+
// so two editors importing the same package don't double-register a file).
|
|
92
|
+
// - `acquiredSpecifiers`: package names already acquired (skip the fetch).
|
|
93
|
+
// - `acquireResetKey`: the install identity (lockfile hash) the current
|
|
94
|
+
// acquired-set belongs to; when it changes, the set is reset so types
|
|
95
|
+
// refresh against the new install.
|
|
96
|
+
const acquiredFilePaths = new Set<string>();
|
|
97
|
+
const acquiredSpecifiers = new Set<string>();
|
|
98
|
+
let currentAcquireResetKey: string | undefined;
|
|
105
99
|
|
|
106
100
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* it is called from module scope here, never from a component render.
|
|
101
|
+
* Reset the acquired-set when the install identity changes. The already-
|
|
102
|
+
* registered extra-libs are left in place (disposing them is unnecessary —
|
|
103
|
+
* the new install re-registers the same virtual paths, and addExtraLib
|
|
104
|
+
* overwrites by path), but the specifier set clears so each package is
|
|
105
|
+
* re-fetched against the new hash.
|
|
113
106
|
*/
|
|
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,
|
|
107
|
+
const syncAcquireResetKey = (resetKey: string | undefined): void => {
|
|
108
|
+
if (resetKey === currentAcquireResetKey) return;
|
|
109
|
+
currentAcquireResetKey = resetKey;
|
|
110
|
+
acquiredSpecifiers.clear();
|
|
111
|
+
acquiredFilePaths.clear();
|
|
143
112
|
};
|
|
144
113
|
|
|
145
114
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* start the service with stale defaults - the timing race the legacy monaco
|
|
150
|
-
* editor hit.
|
|
115
|
+
* Register one acquired package's declaration files with both the TS and JS
|
|
116
|
+
* services, deduped by virtual path. Paths are `node_modules/...`-relative;
|
|
117
|
+
* we mount each at `file:///<path>` so NodeJs + `@types` resolution finds it.
|
|
151
118
|
*/
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
119
|
+
const registerAcquiredFiles = (
|
|
120
|
+
files: ReadonlyArray<{ path: string; content: string }>,
|
|
121
|
+
): void => {
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const uri = `file:///${file.path}`;
|
|
124
|
+
if (acquiredFilePaths.has(uri)) continue;
|
|
125
|
+
acquiredFilePaths.add(uri);
|
|
126
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
127
|
+
defaults.addExtraLib(file.content, uri);
|
|
128
|
+
}
|
|
161
129
|
}
|
|
162
130
|
};
|
|
163
131
|
|
|
164
|
-
configureTypeScriptDefaults();
|
|
165
|
-
|
|
166
132
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* module load; this file is browser-only so the dynamic import is safe here.
|
|
133
|
+
* Acquire types for every NEW bare specifier in `source`, against the given
|
|
134
|
+
* resolver. Pure planning (`parseBareImportSpecifiers` / `planAcquisitions`)
|
|
135
|
+
* is unit-tested; this thin async glue is intentionally untested (no DOM /
|
|
136
|
+
* network in unit tests). A specifier is marked acquired even when it returns
|
|
137
|
+
* no files, so a typeless package isn't re-fetched on every keystroke.
|
|
173
138
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
139
|
+
const runTypeAcquisition = async ({
|
|
140
|
+
source,
|
|
141
|
+
acquireTypes,
|
|
142
|
+
resetKey,
|
|
143
|
+
}: {
|
|
144
|
+
source: string;
|
|
145
|
+
acquireTypes: AcquireTypes;
|
|
146
|
+
resetKey: string | undefined;
|
|
147
|
+
}): Promise<void> => {
|
|
148
|
+
syncAcquireResetKey(resetKey);
|
|
149
|
+
const specifiers = parseBareImportSpecifiers(source);
|
|
150
|
+
const toAcquire = planAcquisitions({
|
|
151
|
+
specifiers,
|
|
152
|
+
acquired: acquiredSpecifiers,
|
|
153
|
+
});
|
|
154
|
+
for (const specifier of toAcquire) {
|
|
155
|
+
// Mark first so concurrent/keystroke re-runs don't double-fetch.
|
|
156
|
+
acquiredSpecifiers.add(specifier);
|
|
157
|
+
try {
|
|
158
|
+
const files = await acquireTypes(specifier);
|
|
159
|
+
registerAcquiredFiles(files);
|
|
160
|
+
} catch {
|
|
161
|
+
// A failed fetch un-marks so a later edit can retry.
|
|
162
|
+
acquiredSpecifiers.delete(specifier);
|
|
185
163
|
}
|
|
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
164
|
}
|
|
193
165
|
};
|
|
194
166
|
|
|
195
|
-
void ensureStandaloneStdlib();
|
|
196
|
-
|
|
197
167
|
// Turn OFF the JSON service's built-in validation. The editor content is a
|
|
198
168
|
// template that renders to JSON, so we validate the template-substituted form
|
|
199
169
|
// ourselves (see the json validation effect + validateJsonTemplate) to tolerate
|
|
@@ -327,6 +297,13 @@ export type TypefoxEditorProps = {
|
|
|
327
297
|
language?: CodeEditorLanguage;
|
|
328
298
|
/** Minimum editor height in pixels. Defaults to 240. */
|
|
329
299
|
minHeight?: number;
|
|
300
|
+
/**
|
|
301
|
+
* When true, the editor container fills its flex parent (`height: 100%`)
|
|
302
|
+
* instead of using a fixed `minHeight` px height, so it grows to fit a tall
|
|
303
|
+
* flex column (e.g. the popout dialog body). `minHeight` is still applied as
|
|
304
|
+
* a floor. Defaults to false, preserving the inline fixed-height behaviour.
|
|
305
|
+
*/
|
|
306
|
+
fillHeight?: boolean;
|
|
330
307
|
/**
|
|
331
308
|
* Generated ambient type definitions (the `context.d.ts`) injected as a TS
|
|
332
309
|
* extra-lib so `context.*` resolves with real fields. Wired up once per
|
|
@@ -356,6 +333,28 @@ export type TypefoxEditorProps = {
|
|
|
356
333
|
readOnly?: boolean;
|
|
357
334
|
/** Accessible label / hint for the editor (surfaced via aria-label). */
|
|
358
335
|
placeholder?: string;
|
|
336
|
+
/**
|
|
337
|
+
* Lazy Automatic Type Acquisition resolver. When provided (TS/JS editors),
|
|
338
|
+
* bare `import`/`require` specifiers in the buffer are parsed (debounced)
|
|
339
|
+
* and each NEW package's `.d.ts` closure is fetched + registered so e.g.
|
|
340
|
+
* `import { debounce } from "lodash"` autocompletes. Injected by the
|
|
341
|
+
* consumer so this component stays plugin-agnostic.
|
|
342
|
+
*/
|
|
343
|
+
acquireTypes?: AcquireTypes;
|
|
344
|
+
/**
|
|
345
|
+
* Install identity (lockfile hash). When it changes, the shared acquired-set
|
|
346
|
+
* resets so types refresh against the new install.
|
|
347
|
+
*/
|
|
348
|
+
acquireResetKey?: string;
|
|
349
|
+
/**
|
|
350
|
+
* Importable installed package NAMES (TS/JS editors). When provided, the
|
|
351
|
+
* editor suggests these as completions while the cursor is inside an import
|
|
352
|
+
* specifier string (`import {} from "lod"` -> `lodash`) - solving the
|
|
353
|
+
* lazy-ATA catch-22 where no module is registered yet. Must already exclude
|
|
354
|
+
* `@types/*` companions (you import `lodash`, never `@types/lodash`).
|
|
355
|
+
* Injected by the consumer so this component stays plugin-agnostic.
|
|
356
|
+
*/
|
|
357
|
+
importablePackages?: string[];
|
|
359
358
|
};
|
|
360
359
|
|
|
361
360
|
/**
|
|
@@ -382,18 +381,30 @@ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
|
|
|
382
381
|
: {};
|
|
383
382
|
})();
|
|
384
383
|
|
|
384
|
+
// Always-available runtime built-in import specifiers (Node + Bun), derived at
|
|
385
|
+
// build time from the bundled stdlib types. These are importable in the script
|
|
386
|
+
// sandbox regardless of the installed-package allowlist (the sandbox is a Bun
|
|
387
|
+
// subprocess; Bun provides Node's builtins + its own `bun:` modules), and their
|
|
388
|
+
// types are already loaded ambiently via the stdlib bundle - so completing one
|
|
389
|
+
// needs no lazy acquisition. The JSON is a plain `string[]`.
|
|
390
|
+
const BUILTIN_MODULE_SPECIFIERS: readonly string[] = builtinModulesJson;
|
|
391
|
+
|
|
385
392
|
export const TypefoxEditor = ({
|
|
386
393
|
id,
|
|
387
394
|
value,
|
|
388
395
|
onChange,
|
|
389
396
|
language = "typescript",
|
|
390
397
|
minHeight = 240,
|
|
398
|
+
fillHeight = false,
|
|
391
399
|
typeDefinitions,
|
|
392
400
|
templateProperties,
|
|
393
401
|
shellEnvVars,
|
|
394
402
|
markers,
|
|
395
403
|
readOnly = false,
|
|
396
404
|
placeholder,
|
|
405
|
+
acquireTypes,
|
|
406
|
+
acquireResetKey,
|
|
407
|
+
importablePackages,
|
|
397
408
|
}: TypefoxEditorProps) => {
|
|
398
409
|
// `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
|
|
399
410
|
// the handler it calls would otherwise close over a stale `onChange` (bound
|
|
@@ -438,6 +449,138 @@ export const TypefoxEditor = ({
|
|
|
438
449
|
};
|
|
439
450
|
}, [isTsLike, typeDefinitions, modelId]);
|
|
440
451
|
|
|
452
|
+
// Lazy Automatic Type Acquisition (ATA). For TS/JS editors with an injected
|
|
453
|
+
// `acquireTypes` resolver, parse the buffer's bare import/require specifiers
|
|
454
|
+
// (debounced) and fetch + register each NEW package's `.d.ts` closure, so
|
|
455
|
+
// `import { x } from "pkg"` autocompletes. The acquired-set is module-scoped
|
|
456
|
+
// and shared across editors (the declarations are install-global); the pure
|
|
457
|
+
// parse/plan steps are unit-tested in importSpecifiers.test.ts. Re-running
|
|
458
|
+
// on a new `acquireTypes`/`acquireResetKey` identity is cheap and safe — the
|
|
459
|
+
// module-scoped acquired-set dedupes, so it never double-fetches.
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (!apiReady || !isTsLike || acquireTypes === undefined) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const model = findModelById(modelId);
|
|
465
|
+
if (!model) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const acquire = (source: string): void => {
|
|
470
|
+
void runTypeAcquisition({
|
|
471
|
+
source,
|
|
472
|
+
acquireTypes,
|
|
473
|
+
resetKey: acquireResetKey,
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Run once for the initial content so existing imports resolve on open.
|
|
478
|
+
acquire(model.getValue());
|
|
479
|
+
|
|
480
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
481
|
+
const subscription = model.onDidChangeContent(() => {
|
|
482
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
483
|
+
timer = setTimeout(() => {
|
|
484
|
+
acquire(model.getValue());
|
|
485
|
+
}, 400);
|
|
486
|
+
});
|
|
487
|
+
return () => {
|
|
488
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
489
|
+
subscription.dispose();
|
|
490
|
+
};
|
|
491
|
+
}, [apiReady, isTsLike, acquireTypes, acquireResetKey, modelId]);
|
|
492
|
+
|
|
493
|
+
// Controlled-value sync. `value` is only seeded into the model at mount
|
|
494
|
+
// (codeResources), so the editor is otherwise uncontrolled. Apply external
|
|
495
|
+
// `value` changes — a sibling editor's edits (the inline ↔ popout pair share
|
|
496
|
+
// one controlled `value`), a YAML→Visual reset, a loaded definition — to this
|
|
497
|
+
// model. Guarded by an equality check: the user's own edit round-trips
|
|
498
|
+
// `value === model.getValue()`, so the actively-edited editor is a no-op and
|
|
499
|
+
// there is no feedback loop; only a background editor whose `value` prop
|
|
500
|
+
// diverged gets updated.
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (!apiReady) return;
|
|
503
|
+
const model = findModelById(modelId);
|
|
504
|
+
if (!model || model.getValue() === value) return;
|
|
505
|
+
model.setValue(value);
|
|
506
|
+
}, [apiReady, modelId, value]);
|
|
507
|
+
|
|
508
|
+
// Import-specifier name completions. Lazy ATA only registers a package's
|
|
509
|
+
// types AFTER its name is in the buffer, so while the user is still TYPING
|
|
510
|
+
// the specifier (`import {} from "lod"`) no module exists yet and the TS
|
|
511
|
+
// worker offers nothing. This provider fills that gap: when the cursor is
|
|
512
|
+
// inside an import/require string (detected by the unit-tested
|
|
513
|
+
// `importSpecifierCompletionContext`), it suggests:
|
|
514
|
+
// - the always-available runtime built-ins (`node:fs`, `bun`, ...), so
|
|
515
|
+
// they appear even with an empty allowlist; AND
|
|
516
|
+
// - the injected installed-package names (already `@types/*`-free).
|
|
517
|
+
// Selecting a built-in inserts an already-typed module; selecting an
|
|
518
|
+
// installed package triggers the ATA loop to load its closure. The list is
|
|
519
|
+
// merged + deduped + sorted by `mergeImportCompletionEntries` (a unit-tested
|
|
520
|
+
// pure helper). Built-ins read as "Node.js" / "Bun built-in" via `detail`.
|
|
521
|
+
// Scoped to THIS model; only the import-string position triggers it, so it
|
|
522
|
+
// never pollutes normal completions. Always registered (built-ins are
|
|
523
|
+
// always available), independent of the allowlist.
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (!apiReady || !isTsLike) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const entries = mergeImportCompletionEntries({
|
|
529
|
+
builtins: BUILTIN_MODULE_SPECIFIERS,
|
|
530
|
+
installedPackages: importablePackages ?? [],
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const provideCompletionItems = (
|
|
534
|
+
model: monaco.editor.ITextModel,
|
|
535
|
+
position: monaco.Position,
|
|
536
|
+
): monaco.languages.CompletionList => {
|
|
537
|
+
if (!model.uri.toString().includes(modelId)) {
|
|
538
|
+
return { suggestions: [] };
|
|
539
|
+
}
|
|
540
|
+
const lineUpToCursor = model
|
|
541
|
+
.getLineContent(position.lineNumber)
|
|
542
|
+
.slice(0, position.column - 1);
|
|
543
|
+
const ctx = importSpecifierCompletionContext(lineUpToCursor);
|
|
544
|
+
if (!ctx) {
|
|
545
|
+
return { suggestions: [] };
|
|
546
|
+
}
|
|
547
|
+
// Replace the whole partial specifier (between the quotes) without
|
|
548
|
+
// touching the quotes themselves.
|
|
549
|
+
const range = {
|
|
550
|
+
startLineNumber: position.lineNumber,
|
|
551
|
+
startColumn: ctx.replaceFromColumn,
|
|
552
|
+
endLineNumber: position.lineNumber,
|
|
553
|
+
endColumn: position.column,
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
suggestions: entries.map((entry) => ({
|
|
557
|
+
label: entry.name,
|
|
558
|
+
kind: monaco.languages.CompletionItemKind.Module,
|
|
559
|
+
detail: entry.detail,
|
|
560
|
+
insertText: entry.name,
|
|
561
|
+
filterText: entry.name,
|
|
562
|
+
range,
|
|
563
|
+
})),
|
|
564
|
+
// The list is the full known set; let monaco filter by `partial`.
|
|
565
|
+
incomplete: false,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
570
|
+
monaco.languages.registerCompletionItemProvider(lang, {
|
|
571
|
+
// Opening quotes start a specifier; `:` advances into a `node:`/`bun:`
|
|
572
|
+
// builtin; `/` advances into a scoped name or subpath.
|
|
573
|
+
triggerCharacters: ['"', "'", "/", ":"],
|
|
574
|
+
provideCompletionItems,
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
return () => {
|
|
578
|
+
for (const disposable of disposables) {
|
|
579
|
+
disposable.dispose();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}, [apiReady, isTsLike, importablePackages, modelId]);
|
|
583
|
+
|
|
441
584
|
// Type-driven bracket-notation completions. The standalone TS worker omits
|
|
442
585
|
// object members whose keys aren't valid identifiers (artifact ids like
|
|
443
586
|
// `integration-jira.issue`), and the built-in SuggestAdapter can't be
|
|
@@ -816,7 +959,7 @@ export const TypefoxEditor = ({
|
|
|
816
959
|
// Plain editor, no workbench views.
|
|
817
960
|
$type: "EditorService",
|
|
818
961
|
},
|
|
819
|
-
monacoWorkerFactory:
|
|
962
|
+
monacoWorkerFactory: ensureStandaloneWorkerFactory,
|
|
820
963
|
};
|
|
821
964
|
|
|
822
965
|
const editorAppConfig: EditorAppConfig = {
|
|
@@ -850,9 +993,17 @@ export const TypefoxEditor = ({
|
|
|
850
993
|
onChangeRef.current?.(textChanges.modified ?? "");
|
|
851
994
|
};
|
|
852
995
|
|
|
996
|
+
// In `fillHeight` mode the container takes its parent's height so Monaco's
|
|
997
|
+
// `automaticLayout` resizes to fill a tall flex column (the popout body);
|
|
998
|
+
// `minHeight` stays as a floor. Otherwise the inline fixed-px behaviour is
|
|
999
|
+
// preserved exactly.
|
|
1000
|
+
const containerStyle: CSSProperties = fillHeight
|
|
1001
|
+
? { minHeight: `${minHeight}px`, height: "100%" }
|
|
1002
|
+
: { minHeight: `${minHeight}px`, height: `${minHeight}px` };
|
|
1003
|
+
|
|
853
1004
|
return (
|
|
854
1005
|
<MonacoEditorReactComp
|
|
855
|
-
style={
|
|
1006
|
+
style={containerStyle}
|
|
856
1007
|
vscodeApiConfig={vscodeApiConfig}
|
|
857
1008
|
editorAppConfig={editorAppConfig}
|
|
858
1009
|
onTextChanged={handleTextChanged}
|
|
@@ -862,6 +1013,11 @@ export const TypefoxEditor = ({
|
|
|
862
1013
|
// once, so a second editor never gets its own onVscodeApiInitDone - but
|
|
863
1014
|
// onEditorStartDone fires for each editor instance.
|
|
864
1015
|
setApiReady(true);
|
|
1016
|
+
// The monaco-vscode services are now up. Let the headless script
|
|
1017
|
+
// validator know it may safely use the worker (it must never init the
|
|
1018
|
+
// services itself - that would collide with this wrapper's one-time
|
|
1019
|
+
// init and throw "Services are already initialized").
|
|
1020
|
+
markVscodeServicesReady();
|
|
865
1021
|
}}
|
|
866
1022
|
/>
|
|
867
1023
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["assert","assert/strict","async_hooks","buffer","bun","bun:bundle","bun:ffi","bun:jsc","bun:sqlite","bun:test","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","dns/promises","domain","events","fs","fs/promises","http","http2","https","inspector","inspector/promises","module","net","node:assert","node:assert/strict","node:async_hooks","node:buffer","node:child_process","node:cluster","node:console","node:constants","node:crypto","node:dgram","node:diagnostics_channel","node:dns","node:dns/promises","node:domain","node:events","node:fs","node:fs/promises","node:http","node:http2","node:https","node:inspector","node:inspector/promises","node:module","node:net","node:os","node:path","node:path/posix","node:path/win32","node:perf_hooks","node:process","node:punycode","node:querystring","node:readline","node:readline/promises","node:repl","node:sea","node:stream","node:stream/consumers","node:stream/promises","node:stream/web","node:string_decoder","node:test","node:test/reporters","node:timers","node:timers/promises","node:tls","node:trace_events","node:tty","node:url","node:util","node:util/types","node:v8","node:vm","node:wasi","node:worker_threads","node:zlib","os","path","path/posix","path/win32","perf_hooks","process","punycode","querystring","readline","readline/promises","repl","stream","stream/consumers","stream/promises","stream/web","string_decoder","timers","timers/promises","tls","trace_events","tty","url","util","util/types","v8","vm","wasi","worker_threads","zlib"]
|